diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index ebb9c3dc86dd2..db19a0a5b249d 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -12,8 +12,12 @@ kibanaPipeline(timeoutMinutes: 240) { ]) { workers.base(name: 'coverage-worker', size: 'l', ramDisk: false, bootstrapped: false) { catchError { + + kibanaPipeline.bash(""" + echo '${TIME_STAMP}' + """, "### Print Canonical Time Stamp") + kibanaCoverage.runTests() - kibanaTeamAssign.load('team_assignment', "### Upload Team Assignment JSON") handleIngestion(TIME_STAMP) } handleFail() @@ -30,7 +34,7 @@ def handleIngestion(timestamp) { kibanaCoverage.collectVcsInfo("### Collect VCS Info") kibanaCoverage.generateReports("### Merge coverage reports") kibanaCoverage.uploadCombinedReports() - kibanaCoverage.ingest(env.JOB_NAME, BUILD_NUMBER, BUILD_URL, timestamp, previousSha, '### Ingest && Upload') + kibanaCoverage.ingest(env.JOB_NAME, BUILD_NUMBER, BUILD_URL, timestamp, previousSha, teamAssignmentsPath(), '### Generate Team Assignments && Ingest') kibanaCoverage.uploadCoverageStaticSite(timestamp) } @@ -42,7 +46,7 @@ def handlePreviousSha() { def handleFail() { def buildStatus = buildUtils.getBuildStatus() - if(params.NOTIFY_ON_FAILURE && buildStatus != 'SUCCESS' && buildStatus != 'ABORTED' && buildStatus != 'UNSTABLE') { + if (params.NOTIFY_ON_FAILURE && buildStatus != 'SUCCESS' && buildStatus != 'ABORTED' && buildStatus != 'UNSTABLE') { slackNotifications.sendFailedBuild( channel: '#kibana-qa', username: 'Kibana QA' @@ -50,3 +54,7 @@ def handleFail() { } } +def teamAssignmentsPath() { + return 'src/dev/code_coverage/ingest_coverage/team_assignment/team_assignments.txt' +} + diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2f5e14f1f1599..54cf52fae5fe8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,6 +2,9 @@ # Identify which groups will be pinged by changes to different parts of the codebase. # For more info, see https://help.github.com/articles/about-codeowners/ +# The #CC# prefix delineates Code Coverage, +# used for the 'team' designator within Kibana Stats + # App /x-pack/plugins/dashboard_enhanced/ @elastic/kibana-app /x-pack/plugins/discover_enhanced/ @elastic/kibana-app @@ -26,6 +29,33 @@ /src/plugins/vis_type_xy/ @elastic/kibana-app /src/plugins/visualize/ @elastic/kibana-app /src/plugins/visualizations/ @elastic/kibana-app +#CC# /src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app +#CC# /src/plugins/vis_type @elastic/kibana-app +#CC# /src/legacy/core_plugins/kibana/ @elastic/kibana-app +#CC# /src/legacy/core_plugins/kibana/common/utils @elastic/kibana-app +#CC# /src/legacy/core_plugins/kibana/migrations @elastic/kibana-app +#CC# /src/legacy/core_plugins/kibana/public @elastic/kibana-app +#CC# /src/legacy/core_plugins/kibana/public/dashboard/ @elastic/kibana-app +#CC# /src/legacy/core_plugins/kibana/public/dev_tools/ @elastic/kibana-app +#CC# /src/legacy/core_plugins/kibana/public/discover/ @elastic/kibana-app +#CC# /src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app +#CC# /src/legacy/core_plugins/console_legacy @elastic/kibana-app +#CC# /src/legacy/core_plugins/input_control_vis @elastic/kibana-app +#CC# /src/legacy/core_plugins/timelion @elastic/kibana-app +#CC# /src/legacy/core_plugins/vis_type_tagcloud @elastic/kibana-app +#CC# /src/legacy/core_plugins/vis_type_vega @elastic/kibana-app +#CC# /src/legacy/core_plugins/vis_type_vislib/ @elastic/kibana-app +#CC# /src/legacy/server/sample_data/ @elastic/kibana-app +#CC# /src/legacy/server/url_shortening/ @elastic/kibana-app +#CC# /src/legacy/ui/public/state_management @elastic/kibana-app +#CC# /src/plugins/charts/public/static/color_maps @elastic/kibana-app +#CC# /src/plugins/index_pattern_management/public @elastic/kibana-app +#CC# /src/plugins/input_control_vis/ @elastic/kibana-app +#CC# /src/plugins/kibana_legacy/ @elastic/kibana-app +#CC# /src/plugins/timelion @elastic/kibana-app +#CC# /x-pack/legacy/plugins/dashboard_mode/ @elastic/kibana-app +#CC# /x-pack/plugins/dashboard_mode @elastic/kibana-app +#CC# /x-pack/plugins/lens/ @elastic/kibana-app # App Architecture /examples/bfetch_explorer/ @elastic/kibana-app-arch @@ -56,12 +86,37 @@ /x-pack/plugins/data_enhanced/ @elastic/kibana-app-arch /x-pack/plugins/embeddable_enhanced/ @elastic/kibana-app-arch /x-pack/plugins/ui_actions_enhanced/ @elastic/kibana-app-arch +#CC# /src/legacy/core_plugins/kibana/public/management/ @elastic/kibana-app-arch +#CC# /src/legacy/core_plugins/kibana/server/routes/api/management/ @elastic/kibana-app-arch +#CC# /src/legacy/core_plugins/embeddable_api/ @elastic/kibana-app-arch +#CC# /src/legacy/core_plugins/interpreter/ @elastic/kibana-app-arch +#CC# /src/legacy/core_plugins/kibana_react/ @elastic/kibana-app-arch +#CC# /src/legacy/core_plugins/status_page/public @elastic/kibana-app-arch +#CC# /src/legacy/server/index_patterns/ @elastic/kibana-app-arch +#CC# /src/legacy/ui/public/field_editor @elastic/kibana-app-arch +#CC# /src/legacy/ui/public/management @elastic/kibana-app-arch +#CC# /src/plugins/advanced_settings/ @elastic/kibana-app-arch +#CC# /src/plugins/bfetch/ @elastic/kibana-app-arch +#CC# /src/plugins/charts/ @elastic/kibana-app-arch +#CC# /src/plugins/index_pattern_management/public/service @elastic/kibana-app-arch +#CC# /src/plugins/inspector/ @elastic/kibana-app-arch +#CC# /src/plugins/saved_objects/ @elastic/kibana-app-arch +#CC# /src/plugins/share/ @elastic/kibana-app-arch +#CC# /src/plugins/vis_default_editor @elastic/kibana-app-arch +#CC# /x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch +#CC# /x-pack/plugins/drilldowns/ @elastic/kibana-app-arch +#CC# /packages/kbn-interpreter/ @elastic/kibana-app-arch # APM /x-pack/plugins/apm/ @elastic/apm-ui /x-pack/test/functional/apps/apm/ @elastic/apm-ui /src/plugins/apm_oss/ @elastic/apm-ui /src/apm.js @watson @vigneshshanmugam +#CC# /src/plugins/apm_oss/ @elastic/apm-ui +#CC# /src/legacy/core_plugins/apm_oss/ @elastic/apm-ui +#CC# /src/legacy/ui/public/apm @elastic/apm-ui +#CC# /x-pack/legacy/plugins/apm/ @elastic/apm-ui +#CC# /x-pack/plugins/observability/ @elastic/apm-ui # Client Side Monitoring (lives in APM directories but owned by Uptime) /x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm @elastic/uptime @@ -71,13 +126,19 @@ /x-pack/plugins/apm/server/lib/rum_client @elastic/uptime /x-pack/plugins/apm/server/routes/rum_client.ts @elastic/uptime /x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts @elastic/uptime +/x-pack/plugins/apm/server/projections/rum_overview.ts @elastic/uptime +#CC# /x-pack/legacy/plugins/uptime @elastic/uptime # Beats /x-pack/plugins/beats_management/ @elastic/beats +/x-pack/legacy/plugins/beats_management/ @elastic/beats +#CC# /x-pack/plugins/beats_management/ @elastic/beats # Canvas /x-pack/plugins/canvas/ @elastic/kibana-canvas /x-pack/test/functional/apps/canvas/ @elastic/kibana-canvas +#CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-canvas +#CC# /x-pack/legacy/plugins/canvas/ @elastic/kibana-canvas # Core UI # Exclude tutorials folder for now because they are not owned by Kibana app and most will move out soon @@ -85,6 +146,12 @@ /src/plugins/home/server/*.ts @elastic/kibana-core-ui /src/plugins/home/server/services/ @elastic/kibana-core-ui /x-pack/plugins/global_search_bar/ @elastic/kibana-core-ui +#CC# /src/legacy/core_plugins/newsfeed @elastic/kibana-core-ui +#CC# /src/plugins/newsfeed @elastic/kibana-core-ui +#CC# /src/plugins/home/public @elastic/kibana-core-ui +#CC# /src/plugins/home/server/services/ @elastic/kibana-core-ui +#CC# /src/plugins/home/ @elastic/kibana-core-ui +#CC# /x-pack/plugins/global_search_providers/ @elastic/kibana-core-ui # Observability UIs /x-pack/plugins/infra/ @elastic/logs-metrics-ui @@ -110,6 +177,14 @@ /x-pack/test/functional/apps/maps/ @elastic/kibana-gis /x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis /x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis +#CC# /src/legacy/core_plugins/region_map @elastic/kibana-gis +#CC# /src/legacy/core_plugins/tile_map @elastic/kibana-gis +#CC# /src/plugins/maps_legacy/ @elastic/kibana-gis +#CC# /x-pack/plugins/file_upload @elastic/kibana-gis +#CC# /x-pack/plugins/maps_legacy_licensing @elastic/kibana-gis +#CC# /src/plugins/home/server/tutorials @elastic/kibana-gis +#CC# /src/plugins/tile_map/ @elastic/kibana-gis +#CC# /src/plugins/region_map/ @elastic/kibana-gis # Operations /src/dev/ @elastic/kibana-operations @@ -132,6 +207,7 @@ /src/legacy/server/warnings/ @elastic/kibana-operations /.ci/es-snapshots/ @elastic/kibana-operations /vars/ @elastic/kibana-operations +#CC# /packages/kbn-expect/ @elastic/kibana-operations # Quality Assurance /src/dev/code_coverage @elastic/kibana-qa @@ -158,6 +234,31 @@ /src/plugins/status_page/ @elastic/kibana-platform /src/plugins/saved_objects_management/ @elastic/kibana-platform /src/dev/run_check_published_api_changes.ts @elastic/kibana-platform +#CC# /src/core/server/csp/ @elastic/kibana-platform +#CC# /src/legacy/core_plugins/kibana/server/lib @elastic/kibana-platform +#CC# /src/legacy/core_plugins/kibana/server/lib/management/saved_objects @elastic/kibana-platform +#CC# /src/legacy/core_plugins/kibana/server/routes/api/import/ @elastic/kibana-platform +#CC# /src/legacy/core_plugins/kibana/server/routes/api/export/ @elastic/kibana-platform +#CC# /src/legacy/core_plugins/elasticsearch @elastic/kibana-platform +#CC# /src/legacy/core_plugins/testbed @elastic/kibana-platform +#CC# /src/legacy/server/config/ @elastic/kibana-platform +#CC# /src/legacy/server/http/ @elastic/kibana-platform +#CC# /src/legacy/server/status/ @elastic/kibana-platform +#CC# /src/legacy/ui/public/new_platform @elastic/kibana-platform +#CC# /src/legacy/ui/public/plugin_discovery @elastic/kibana-platform +#CC# /src/legacy/ui/public/chrome @elastic/kibana-platform +#CC# /src/legacy/ui/public/notify @elastic/kibana-platform +#CC# /src/legacy/ui/public/documentation_links @elastic/kibana-platform +#CC# /src/legacy/ui/public/autoload @elastic/kibana-platform +#CC# /src/plugins/legacy_export/ @elastic/kibana-platform +#CC# /src/plugins/status_page/ @elastic/kibana-platform +#CC# /src/plugins/testbed/server/ @elastic/kibana-platform +#CC# /x-pack/legacy/plugins/xpack_main/server/ @elastic/kibana-platform +#CC# /x-pack/legacy/server/lib/ @elastic/kibana-platform +#CC# /x-pack/plugins/cloud/ @elastic/kibana-platform +#CC# /x-pack/plugins/features/ @elastic/kibana-platform +#CC# /x-pack/plugins/global_search/ @elastic/kibana-platform +#CC# /src/legacy/plugin_discovery/ @elastic/kibana-platform # Security /src/core/server/csp/ @elastic/kibana-security @elastic/kibana-platform @@ -177,12 +278,19 @@ /x-pack/test/security_functional/ @elastic/kibana-security /x-pack/test/spaces_api_integration/ @elastic/kibana-security /x-pack/test/token_api_integration/ @elastic/kibana-security +#CC# /src/legacy/ui/public/capabilities @elastic/kibana-security +#CC# /x-pack/legacy/plugins/encrypted_saved_objects/ @elastic/kibana-security +#CC# /x-pack/plugins/security_solution/ @elastic/kibana-security +#CC# /x-pack/plugins/security/ @elastic/kibana-security +#CC# /x-pack/plugins/audit_trail/ @elastic/kibana-security # Kibana Localization /src/dev/i18n/ @elastic/kibana-localization /src/legacy/server/i18n/ @elastic/kibana-localization /src/core/public/i18n/ @elastic/kibana-localization /packages/kbn-i18n/ @elastic/kibana-localization +#CC# /src/legacy/server/i18n/ @elastic/kibana-localization +#CC# /x-pack/plugins/translations/ @elastic/kibana-localization # Kibana Telemetry /packages/kbn-analytics/ @elastic/kibana-telemetry @@ -211,6 +319,11 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/plugins/triggers_actions_ui/ @elastic/kibana-alerting-services /x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/ @elastic/kibana-alerting-services /x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/ @elastic/kibana-alerting-services +#CC# /x-pack/legacy/plugins/actions/ @elastic/kibana-alerting-services +#CC# /x-pack/legacy/plugins/alerting/ @elastic/kibana-alerting-services +#CC# /x-pack/legacy/plugins/task_manager @elastic/kibana-alerting-services +#CC# /x-pack/legacy/plugins/triggers_actions_ui/ @elastic/kibana-alerting-services +#CC# /x-pack/plugins/alerting_builtins @elastic/kibana-alerting-services # Enterprise Search # Shared @@ -248,6 +361,12 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/plugins/ingest_pipelines/ @elastic/es-ui /packages/kbn-ace/ @elastic/es-ui /packages/kbn-monaco/ @elastic/es-ui +#CC# /x-pack/legacy/plugins/rollup/ @elastic/es-ui +#CC# /x-pack/legacy/server/lib/create_router/ @elastic/es-ui +#CC# /x-pack/legacy/server/lib/check_license/ @elastic/es-ui +#CC# /x-pack/plugins/console_extensions/ @elastic/es-ui +#CC# /x-pack/plugins/cross_cluster_replication/ @elastic/es-ui +#CC# /x-pack/plugins/es_ui_shared/ @elastic/es-u # Endpoint /x-pack/plugins/endpoint/ @elastic/endpoint-app-team @elastic/siem @@ -257,6 +376,9 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/test/functional/es_archives/endpoint/ @elastic/endpoint-app-team @elastic/siem /x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/endpoint-app-team @elastic/siem /x-pack/test/plugin_functional/test_suites/resolver/ @elastic/endpoint-app-team @elastic/siem +#CC# /x-pack/legacy/plugins/siem/ @elastic/siem +#CC# /x-pack/plugins/siem/ @elastic/siem +#CC# /x-pack/plugins/security_solution/ @elastic/siem # Security Solution /x-pack/plugins/security_solution/ @elastic/siem @elastic/endpoint-app-team @@ -271,6 +393,7 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Design (at the bottom for specificity of SASS files) **/*.scss @elastic/kibana-design +#CC# /packages/kbn-ui-framework/ @elastic/kibana-design # Core design /src/plugins/dashboard/**/*.scss @elastic/kibana-core-ui-designers @@ -292,3 +415,9 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/plugins/endpoint/**/*.scss @elastic/security-design /x-pack/plugins/security_solution/**/*.scss @elastic/security-design +# Logstash +#CC# /x-pack/plugins/logstash/ @elastic/logstash + +# Reporting +#CC# /x-pack/plugins/reporting/ @elastic/kibana-reporting-services + diff --git a/docs/canvas/canvas-edit-workpads.asciidoc b/docs/canvas/canvas-edit-workpads.asciidoc index 6558def8a7474..6ad2d89be4a42 100644 --- a/docs/canvas/canvas-edit-workpads.asciidoc +++ b/docs/canvas/canvas-edit-workpads.asciidoc @@ -25,12 +25,12 @@ For example, to change the index pattern for a set of charts: Specify the variable options. [role="screenshot"] -image::images/specify_variable_syntax.png[Specify the variable syntax] +image::images/specify_variable_syntax.png[Image describing how to specify the variable syntax] Copy the variable, then apply it to each element you want to update in the *Expression editor*. [role="screenshot"] -image::images/copy_variable_syntax.png[Copy the variable syntax] +image::images/copy_variable_syntax.png[Image demonstrating expression editor] [float] [[apply-changes-to-the-entire-workpad]] @@ -85,7 +85,7 @@ To use an element with the same functionality and appearance in multiple places, Select the element, then click *Edit > Clone*. [role="screenshot"] -image::images/clone_element.gif[Clone elements] +image::images/clone_element.gif[Image showing how to clone elements] [float] [[move-and-resize-elements]] diff --git a/docs/canvas/canvas-expression-lifecycle.asciidoc b/docs/canvas/canvas-expression-lifecycle.asciidoc index 895c1382c4d36..7d48c593f9e18 100644 --- a/docs/canvas/canvas-expression-lifecycle.asciidoc +++ b/docs/canvas/canvas-expression-lifecycle.asciidoc @@ -30,7 +30,7 @@ The filtered <> becomes the _context_ of the next functi Let’s look at another expression, which uses the same <> function, but instead produces a pie chart. -image::images/canvas-functions-can-take-arguments-pie-chart.png[Pie Chart, height=400] +image::images/canvas-functions-can-take-arguments-pie-chart.png[Pie chart showing output of demodata function] [source,text] ---- filters @@ -47,7 +47,7 @@ If the expression stopped there, it would produce a `pointseries` data type as t The end result is a simple pie chart that uses the default color palette, but the <> function can take additional arguments that control how it gets rendered. For example, you can provide a `hole` argument to turn your pie chart into a donut chart by changing the expression to: -image::images/canvas-functions-can-take-arguments-donut-chart.png[Donut Chart, height=400] +image::images/canvas-functions-can-take-arguments-donut-chart.png[Alternative output as donut chart] [source,text] ---- filters @@ -83,7 +83,7 @@ You can substitute one function for another to change the output. For example, y Let’s change that last pie chart into a bubble chart by replacing the <> function with the <> function. This is possible because both functions can accept a `pointseries` data type as their _context_. Switching the functions will work, but it won’t produce a useful visualization on its own since you don’t have the x-axis and y-axis defined. You will also need to modify the <> function to change its output. In this case, you can change the `size` argument to `y`, so the maximum price values are plotted on the y-axis, and add an `x` argument using the `@timestamp` field in the data to plot those values over time. This leaves you with the following expression and produces a bubble chart showing the max price of each state over time: -image::images/canvas-change-your-expression-chart.png[Bubble Chart, height=400] +image::images/canvas-change-your-expression-chart.png[Bubble Chart, with price along x axis, and time along y axis] [source,text] ---- filters @@ -95,7 +95,7 @@ filters Similar to the <> function, the <> function takes arguments that control the design elements of the visualization. As one example, passing a `legend` argument with a value of `false` to the function will hide the legend on the chart. -image::images/canvas-change-your-expression-chart-no-legend.png[Bubble Chart Without Legend, height=400] +image::images/canvas-change-your-expression-chart-no-legend.png[Bubble Chart Without Legend] [source,text,subs=+quotes] ---- filters diff --git a/docs/canvas/canvas-present-workpad.asciidoc b/docs/canvas/canvas-present-workpad.asciidoc index a6d801b74fce1..b1492f57e46f8 100644 --- a/docs/canvas/canvas-present-workpad.asciidoc +++ b/docs/canvas/canvas-present-workpad.asciidoc @@ -18,7 +18,7 @@ image::images/canvas-autoplay-interval.png[Element autoplay interval] . To start your presentation, click *View > Enter fullscreen mode*. + [role="screenshot"] -image::images/canvas-fullscreen.png[Fullscreen mode] +image::images/canvas-fullscreen.png[Image showing how to enter fullscreen mode from view dropdown] . When you are ready to exit fullscreen mode, press the Esc (Escape) key. @@ -33,7 +33,7 @@ To get a closer look at a portion of your workpad, use the zoom options. . Select the zoom option. + [role="screenshot"] -image::images/canvas-zoom-controls.png[Zoom controls] +image::images/canvas-zoom-controls.png[Zoom controls, also in view dropdown] [float] [[configure-auto-refresh-interval]] diff --git a/docs/canvas/canvas-share-workpad.asciidoc b/docs/canvas/canvas-share-workpad.asciidoc index f6cd2d93a9372..4887eb6ca870d 100644 --- a/docs/canvas/canvas-share-workpad.asciidoc +++ b/docs/canvas/canvas-share-workpad.asciidoc @@ -13,7 +13,7 @@ Create a JSON file of your workpad that you can export outside of {kib}. Click *Share > Download as JSON*. [role="screenshot"] -image::images/canvas-export-workpad.png[Export single workpad] +image::images/canvas-export-workpad.png[Export single workpad through JSON, from Share dropdown] Want to export multiple workpads? Go to the *Canvas* home page, select the workpads you want to export, then click *Export*. @@ -26,7 +26,7 @@ If you have a subscription that supports the {report-features}, you can create a Click *Share > PDF reports > Generate PDF*. [role="screenshot"] -image::images/canvas-generate-pdf.gif[Generate PDF] +image::images/canvas-generate-pdf.gif[Image showing how to generate a PDF] For more information, refer to <>. @@ -39,7 +39,7 @@ If you have a subscription that supports the {report-features}, you can create a Click *Share > PDF reports > Copy POST URL*. [role="screenshot"] -image::images/canvas-create-URL.gif[Create POST URL] +image::images/canvas-create-URL.gif[Image showing how to create POST URL] For more information, refer to <>. @@ -58,7 +58,7 @@ beta[] Canvas allows you to create _shareables_, which are workpads that you dow To make sure that your data remains secure, the data in the JSON file is not connected to {kib}. Canvas does not display elements that manipulate the data on the workpad. + [role="screenshot"] -image::canvas/images/canvas-embed_workpad.gif[Share the workpad on a website] +image::canvas/images/canvas-embed_workpad.gif[Image showing how to share the workpad on a website] + NOTE: Shareable workpads encode the current state of the workpad in a JSON file. When you make changes to the workpad, the changes do not appear in the shareable workpad on your website. diff --git a/docs/canvas/canvas-tutorial.asciidoc b/docs/canvas/canvas-tutorial.asciidoc index a861b30db784f..ea4d2c8cc6a83 100644 --- a/docs/canvas/canvas-tutorial.asciidoc +++ b/docs/canvas/canvas-tutorial.asciidoc @@ -30,7 +30,7 @@ The default Elastic logo image appears on the page. . To replace the Elastic logo with your own image, select the image, then use the editor. [role="screenshot"] -image::images/canvas-image-element.png[] +image::images/canvas-image-element.png[Image showing how to add the image element] [float] === Customize your data with metrics @@ -70,7 +70,7 @@ You're now looking at the raw data syntax that Canvas uses to display the elemen .. Click *Run*. [role="screenshot"] -image::images/canvas-metric-element.png[] +image::images/canvas-metric-element.png[Image showing changes to the Canvas workpad] [float] === Show off your data with charts @@ -96,7 +96,7 @@ To show what your data can do, add charts, graphs, progress monitors, and more t .. From the *Y-axis* drop-down lists, select *Value*, then select *taxless_total_price*. [role="screenshot"] -image::images/canvas-chart-element.png[] +image::images/canvas-chart-element.png[Image showing Canvas workpad with sample data graph] [float] === Show how your data changes over time @@ -110,7 +110,7 @@ To focus your data on a specific time range, add the time filter. . To use the date time field from the sample data, enter `order_date` in the *Column* field, then click *Set*. [role="screenshot"] -image::images/canvas-timefilter-element.png[] +image::images/canvas-timefilter-element.png[Image showing Canvas workpad with filtered sample data graph] To see how the data changes, set the time filter to *Last 7 days*. As you change the time filter options, the elements automatically update. diff --git a/docs/developer/advanced/development-basepath.asciidoc b/docs/developer/advanced/development-basepath.asciidoc index cb341b9591174..30086288de834 100644 --- a/docs/developer/advanced/development-basepath.asciidoc +++ b/docs/developer/advanced/development-basepath.asciidoc @@ -7,11 +7,11 @@ You can set this explicitly using `server.basePath` in <>. Use the server.rewriteBasePath setting to tell {kib} if it should remove the basePath from requests it receives, and to prevent a deprecation warning at startup. This setting cannot end in a slash (/). -If you want to turn off the basepath when in development mode, start {kib} with the `--no-basepath` flag +If you want to turn off the basepath when in development mode, start {kib} with the `--no-base-path` flag [source,bash] ---- -yarn start --no-basepath +yarn start --no-base-path ---- diff --git a/docs/developer/architecture/development-plugin-saved-objects.asciidoc b/docs/developer/architecture/development-plugin-saved-objects.asciidoc new file mode 100644 index 0000000000000..0d31f5d90f668 --- /dev/null +++ b/docs/developer/architecture/development-plugin-saved-objects.asciidoc @@ -0,0 +1,311 @@ +[[development-plugin-saved-objects]] +== Using Saved Objects + +Saved Objects allow {kib} plugins to use {es} like a primary +database. Think of it as an Object Document Mapper for {es}. Once a +plugin has registered one or more Saved Object types, the Saved Objects client +can be used to query or perform create, read, update and delete operations on +each type. + +By using Saved Objects your plugin can take advantage of the following +features: + +* Migrations can evolve your document's schema by transforming documents and +ensuring that the field mappings on the index are always up to date. +* a <> is automatically exposed for each type (unless +`hidden=true` is specified). +* a Saved Objects client that can be used from both the server and the browser. +* Users can import or export Saved Objects using the Saved Objects management +UI or the Saved Objects import/export API. +* By declaring `references`, an object's entire reference graph will be +exported. This makes it easy for users to export e.g. a `dashboard` object and +have all the `visualization` objects required to display the dashboard +included in the export. +* When the X-Pack security and spaces plugins are enabled these transparently +provide RBAC access control and the ability to organize Saved Objects into +spaces. + +This document contains developer guidelines and best-practices for plugins +wanting to use Saved Objects. + +=== Registering a Saved Object type +Saved object type definitions should be defined in their own `my_plugin/server/saved_objects` directory. + +The folder should contain a file per type, named after the snake_case name of the type, and an `index.ts` file exporting all the types. + +.src/plugins/my_plugin/server/saved_objects/dashboard_visualization.ts +[source,typescript] +---- +import { SavedObjectsType } from 'src/core/server'; + +export const dashboardVisualization: SavedObjectsType = { + name: 'dashboard_visualization', // <1> + hidden: false, + namespaceType: 'single', + mappings: { + dynamic: false, + properties: { + description: { + type: 'text', + }, + hits: { + type: 'integer', + }, + }, + }, + migrations: { + '1.0.0': migratedashboardVisualizationToV1, + '2.0.0': migratedashboardVisualizationToV2, + }, +}; +---- +<1> Since the name of a Saved Object type forms part of the url path for the +public Saved Objects HTTP API, these should follow our API URL path convention +and always be written as snake case. + +.src/plugins/my_plugin/server/saved_objects/index.ts +[source,typescript] +---- +export { dashboardVisualization } from './dashboard_visualization'; +export { dashboard } from './dashboard'; +---- + +.src/plugins/my_plugin/server/plugin.ts +[source,typescript] +---- +import { dashboard, dashboardVisualization } from './saved_objects'; + +export class MyPlugin implements Plugin { + setup({ savedObjects }) { + savedObjects.registerType(dashboard); + savedObjects.registerType(dashboardVisualization); + } +} +---- + +=== Mappings +Each Saved Object type can define it's own {es} field mappings. +Because multiple Saved Object types can share the same index, mappings defined +by a type will be nested under a top-level field that matches the type name. + +For example, the mappings defined by the `dashboard_visualization` Saved +Object type: + +.src/plugins/my_plugin/server/saved_objects/dashboard_visualization.ts +[source,typescript] +---- +import { SavedObjectsType } from 'src/core/server'; + +export const dashboardVisualization: SavedObjectsType = { + name: 'dashboard_visualization', + ... + mappings: { + properties: { + dynamic: false, + description: { + type: 'text', + }, + hits: { + type: 'integer', + }, + }, + }, + migrations: { ... }, +}; +---- + +Will result in the following mappings being applied to the `.kibana` index: +[source,json] +---- +{ + "mappings": { + "dynamic": "strict", + "properties": { + ... + "dashboard_vizualization": { + "dynamic": false, + "properties": { + "description": { + "type": "text", + }, + "hits": { + "type": "integer", + }, + }, + } + } + } +} +---- + +Do not use field mappings like you would use data types for the columns of a +SQL database. Instead, field mappings are analogous to a SQL index. Only +specify field mappings for the fields you wish to search on or query. By +specifying `dynamic: false` in any level of your mappings, {es} will +accept and store any other fields even if they are not specified in your mappings. + +Since {es} has a default limit of 1000 fields per index, plugins +should carefully consider the fields they add to the mappings. Similarly, +Saved Object types should never use `dynamic: true` as this can cause an +arbitrary amount of fields to be added to the `.kibana` index. + +=== References +When a Saved Object declares `references` to other Saved Objects, the +Saved Objects Export API will automatically export the target object with all +of it's references. This makes it easy for users to export the entire +reference graph of an object. + +If a Saved Object can't be used on it's own, that is, it needs other objects +to exist for a feature to function correctly, that Saved Object should declare +references to all the objects it requires. For example, a `dashboard` +object might have panels for several `visualization` objects. When these +`visualization` objects don't exist, the dashboard cannot be rendered +correctly. The `dashboard` object should declare references to all it's +visualizations. + +However, `visualization` objects can continue to be rendered or embedded into +other dashboards even if the `dashboard` it was originally embedded into +doesn't exist. As a result, `visualization` objects should not declare +references to `dashboard` objects. + +For each referenced object, an `id`, `type` and `name` are added to the +`references` array: + +[source, typescript] +---- +router.get( + { path: '/some-path', validate: false }, + async (context, req, res) => { + const object = await context.core.savedObjects.client.create( + 'dashboard', + { + title: 'my dashboard', + panels: [ + { visualization: 'vis1' }, // <1> + ], + indexPattern: 'indexPattern1' + }, + { references: [ + { id: '...', type: 'visualization', name: 'vis1' }, + { id: '...', type: 'index_pattern', name: 'indexPattern1' }, + ] + } + ) + ... + } +); +---- +<1> Note how `dashboard.panels[0].visualization` stores the `name` property of +the reference (not the `id` directly) to be able to uniquely identify this +reference. This guarantees that the id the reference points to always remains +up to date. If a visualization `id` was directly stored in +`dashboard.panels[0].visualization` there is a risk that this `id` gets +updated without updating the reference in the references array. + +==== Writing Migrations + +Saved Objects support schema changes between Kibana versions, which we call +migrations. Migrations are applied when a Kibana installation is upgraded from +one version to the next, when exports are imported via the Saved Objects +Management UI, or when a new object is created via the HTTP API. + +Each Saved Object type may define migrations for its schema. Migrations are +specified by the Kibana version number, receive an input document, and must +return the fully migrated document to be persisted to Elasticsearch. + +Let's say we want to define two migrations: +- In version 1.1.0, we want to drop the `subtitle` field and append it to the + title +- In version 1.4.0, we want to add a new `id` field to every panel with a newly + generated UUID. + +First, the current `mappings` should always reflect the latest or "target" +schema. Next, we should define a migration function for each step in the schema +evolution: + +src/plugins/my_plugin/server/saved_objects/dashboard_visualization.ts +[source,typescript] +---- +import { SavedObjectsType, SavedObjectMigrationFn } from 'src/core/server'; +import uuid from 'uuid'; + +interface DashboardVisualizationPre110 { + title: string; + subtitle: string; + panels: Array<{}>; +} +interface DashboardVisualization110 { + title: string; + panels: Array<{}>; +} + +interface DashboardVisualization140 { + title: string; + panels: Array<{ id: string }>; +} + +const migrateDashboardVisualization110: SavedObjectMigrationFn< + DashboardVisualizationPre110, // <1> + DashboardVisualization110 +> = (doc) => { + const { subtitle, ...attributesWithoutSubtitle } = doc.attributes; + return { + ...doc, // <2> + attributes: { + ...attributesWithoutSubtitle, + title: `${doc.attributes.title} - ${doc.attributes.subtitle}`, + }, + }; +}; + +const migrateDashboardVisualization140: SavedObjectMigrationFn< + DashboardVisualization110, + DashboardVisualization140 +> = (doc) => { + const outPanels = doc.attributes.panels?.map((panel) => { + return { ...panel, id: uuid.v4() }; + }); + return { + ...doc, + attributes: { + ...doc.attributes, + panels: outPanels, + }, + }; +}; + +export const dashboardVisualization: SavedObjectsType = { + name: 'dashboard_visualization', // <1> + /** ... */ + migrations: { + // Takes a pre 1.1.0 doc, and converts it to 1.1.0 + '1.1.0': migrateDashboardVisualization110, + + // Takes a 1.1.0 doc, and converts it to 1.4.0 + '1.4.0': migrateDashboardVisualization140, // <3> + }, +}; +---- +<1> It is useful to define an interface for each version of the schema. This +allows TypeScript to ensure that you are properly handling the input and output +types correctly as the schema evolves. +<2> Returning a shallow copy is necessary to avoid type errors when using +different types for the input and output shape. +<3> Migrations do not have to be defined for every version. The version number +of a migration must always be the earliest Kibana version in which this +migration was released. So if you are creating a migration which will be +part of the v7.10.0 release, but will also be backported and released as +v7.9.3, the migration version should be: 7.9.3. + +Migrations should be written defensively, an exception in a migration function +will prevent a Kibana upgrade from succeeding and will cause downtime for our +users. Having said that, if a document is encountered that is not in the +expected shape, migrations are encouraged to throw an exception to abort the +upgrade. In most scenarios, it is better to fail an upgrade than to silently +ignore a corrupt document which can cause unexpected behaviour at some future +point in time. + +It is critical that you have extensive tests to ensure that migrations behave +as expected with all possible input documents. Given how simple it is to test +all the branch conditions in a migration function and the high impact of a bug +in this code, there's really no reason not to aim for 100% test code coverage. \ No newline at end of file diff --git a/docs/developer/architecture/index.asciidoc b/docs/developer/architecture/index.asciidoc index ac25fe003df08..dc15b90b69d1a 100644 --- a/docs/developer/architecture/index.asciidoc +++ b/docs/developer/architecture/index.asciidoc @@ -15,12 +15,16 @@ A few services also automatically generate api documentation which can be browse A few notable services are called out below. * <> +* <> * <> * <> +include::security/index.asciidoc[leveloffset=+1] + +include::development-plugin-saved-objects.asciidoc[leveloffset=+1] + include::add-data-tutorials.asciidoc[leveloffset=+1] include::development-visualize-index.asciidoc[leveloffset=+1] -include::security/index.asciidoc[leveloffset=+1] diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md index cc3468531fffa..99d2fc00a6b7b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md @@ -9,7 +9,7 @@ Add scripted field to field list Signature: ```typescript -addScriptedField(name: string, script: string, fieldType?: string, lang?: string): Promise; +addScriptedField(name: string, script: string, fieldType?: string): Promise; ``` ## Parameters @@ -19,7 +19,6 @@ addScriptedField(name: string, script: string, fieldType?: string, lang?: string | name | string | | | script | string | | | fieldType | string | | -| lang | string | | Returns: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.istimebasedwildcard.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.istimebasedwildcard.md deleted file mode 100644 index 27f99f418a078..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.istimebasedwildcard.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [isTimeBasedWildcard](./kibana-plugin-plugins-data-public.indexpattern.istimebasedwildcard.md) - -## IndexPattern.isTimeBasedWildcard() method - -Signature: - -```typescript -isTimeBasedWildcard(): boolean; -``` -Returns: - -`boolean` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index 2ff575bc4fc22..325b97383e328 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -41,7 +41,7 @@ export declare class IndexPattern implements IIndexPattern | Method | Modifiers | Description | | --- | --- | --- | -| [addScriptedField(name, script, fieldType, lang)](./kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md) | | Add scripted field to field list | +| [addScriptedField(name, script, fieldType)](./kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md) | | Add scripted field to field list | | [getAggregationRestrictions()](./kibana-plugin-plugins-data-public.indexpattern.getaggregationrestrictions.md) | | | | [getAsSavedObjectBody()](./kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md) | | Returns index pattern as saved object body for saving | | [getComputedFields()](./kibana-plugin-plugins-data-public.indexpattern.getcomputedfields.md) | | | @@ -52,7 +52,6 @@ export declare class IndexPattern implements IIndexPattern | [getSourceFiltering()](./kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md) | | Get the source filtering configuration for that index. | | [getTimeField()](./kibana-plugin-plugins-data-public.indexpattern.gettimefield.md) | | | | [isTimeBased()](./kibana-plugin-plugins-data-public.indexpattern.istimebased.md) | | | -| [isTimeBasedWildcard()](./kibana-plugin-plugins-data-public.indexpattern.istimebasedwildcard.md) | | | | [isTimeNanosBased()](./kibana-plugin-plugins-data-public.indexpattern.istimenanosbased.md) | | | | [popularizeField(fieldName, unit)](./kibana-plugin-plugins-data-public.indexpattern.popularizefield.md) | | | | [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md) | | Remove scripted field from field list | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md index cee213fc6e7e3..5defe4a647614 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md @@ -19,4 +19,5 @@ export interface ISearchStart | [aggs](./kibana-plugin-plugins-data-public.isearchstart.aggs.md) | AggsStart | agg config sub service [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) | | [search](./kibana-plugin-plugins-data-public.isearchstart.search.md) | ISearchGeneric | low level search [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | [searchSource](./kibana-plugin-plugins-data-public.isearchstart.searchsource.md) | ISearchStartSearchSource | high level search [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | +| [showError](./kibana-plugin-plugins-data-public.isearchstart.showerror.md) | (e: Error) => void | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.showerror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.showerror.md new file mode 100644 index 0000000000000..fb14057d83d5c --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.showerror.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) > [showError](./kibana-plugin-plugins-data-public.isearchstart.showerror.md) + +## ISearchStart.showError property + +Signature: + +```typescript +showError: (e: Error) => void; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 0f45b5a727676..e5f56a1ec387f 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -19,10 +19,11 @@ | [IndexPatternSelect](./kibana-plugin-plugins-data-public.indexpatternselect.md) | | | [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) | | | [OptionedParamType](./kibana-plugin-plugins-data-public.optionedparamtype.md) | | +| [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) | | | [Plugin](./kibana-plugin-plugins-data-public.plugin.md) | | -| [RequestTimeoutError](./kibana-plugin-plugins-data-public.requesttimeouterror.md) | Class used to signify that a request timed out. Useful for applications to conditionally handle this type of error differently than other errors. | | [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) | | | [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) | \* | +| [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md) | Request Failure - When an entire multi request fails | | [TimeHistory](./kibana-plugin-plugins-data-public.timehistory.md) | | ## Enumerations @@ -35,6 +36,7 @@ | [METRIC\_TYPES](./kibana-plugin-plugins-data-public.metric_types.md) | | | [QuerySuggestionTypes](./kibana-plugin-plugins-data-public.querysuggestiontypes.md) | | | [SortDirection](./kibana-plugin-plugins-data-public.sortdirection.md) | | +| [TimeoutErrorMode](./kibana-plugin-plugins-data-public.timeouterrormode.md) | | ## Functions diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror._constructor_.md new file mode 100644 index 0000000000000..f8966572afbb6 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror._constructor_.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) > [(constructor)](./kibana-plugin-plugins-data-public.painlesserror._constructor_.md) + +## PainlessError.(constructor) + +Constructs a new instance of the `PainlessError` class + +Signature: + +```typescript +constructor(err: EsError, request: IKibanaSearchRequest); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| err | EsError | | +| request | IKibanaSearchRequest | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.geterrormessage.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.geterrormessage.md new file mode 100644 index 0000000000000..a3b4c51c6c331 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.geterrormessage.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) > [getErrorMessage](./kibana-plugin-plugins-data-public.painlesserror.geterrormessage.md) + +## PainlessError.getErrorMessage() method + +Signature: + +```typescript +getErrorMessage(application: ApplicationStart): JSX.Element; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| application | ApplicationStart | | + +Returns: + +`JSX.Element` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.md new file mode 100644 index 0000000000000..306211cd60259 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.md @@ -0,0 +1,30 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) + +## PainlessError class + +Signature: + +```typescript +export declare class PainlessError extends KbnError +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(err, request)](./kibana-plugin-plugins-data-public.painlesserror._constructor_.md) | | Constructs a new instance of the PainlessError class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [painlessStack](./kibana-plugin-plugins-data-public.painlesserror.painlessstack.md) | | string | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [getErrorMessage(application)](./kibana-plugin-plugins-data-public.painlesserror.geterrormessage.md) | | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.painlessstack.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.painlessstack.md new file mode 100644 index 0000000000000..a7e6920b2ae21 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.painlessstack.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) > [painlessStack](./kibana-plugin-plugins-data-public.painlesserror.painlessstack.md) + +## PainlessError.painlessStack property + +Signature: + +```typescript +painlessStack?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.requesttimeouterror._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.requesttimeouterror._constructor_.md deleted file mode 100644 index 25e472817b46d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.requesttimeouterror._constructor_.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [RequestTimeoutError](./kibana-plugin-plugins-data-public.requesttimeouterror.md) > [(constructor)](./kibana-plugin-plugins-data-public.requesttimeouterror._constructor_.md) - -## RequestTimeoutError.(constructor) - -Constructs a new instance of the `RequestTimeoutError` class - -Signature: - -```typescript -constructor(message?: string); -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| message | string | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.requesttimeouterror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.requesttimeouterror.md deleted file mode 100644 index 84b2fc3fe0b17..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.requesttimeouterror.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [RequestTimeoutError](./kibana-plugin-plugins-data-public.requesttimeouterror.md) - -## RequestTimeoutError class - -Class used to signify that a request timed out. Useful for applications to conditionally handle this type of error differently than other errors. - -Signature: - -```typescript -export declare class RequestTimeoutError extends Error -``` - -## Constructors - -| Constructor | Modifiers | Description | -| --- | --- | --- | -| [(constructor)(message)](./kibana-plugin-plugins-data-public.requesttimeouterror._constructor_.md) | | Constructs a new instance of the RequestTimeoutError class | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md new file mode 100644 index 0000000000000..8ecd8b8c5ac22 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [getTimeoutMode](./kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md) + +## SearchInterceptor.getTimeoutMode() method + +Signature: + +```typescript +protected getTimeoutMode(): TimeoutErrorMode; +``` +Returns: + +`TimeoutErrorMode` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md new file mode 100644 index 0000000000000..02db74b1a9e91 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [handleSearchError](./kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md) + +## SearchInterceptor.handleSearchError() method + +Signature: + +```typescript +protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, appAbortSignal?: AbortSignal): Error; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| e | any | | +| request | IKibanaSearchRequest | | +| timeoutSignal | AbortSignal | | +| appAbortSignal | AbortSignal | | + +Returns: + +`Error` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md index 5cee345db6cd2..a02a6116d7ae0 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md @@ -21,11 +21,13 @@ export declare class SearchInterceptor | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [deps](./kibana-plugin-plugins-data-public.searchinterceptor.deps.md) | | SearchInterceptorDeps | | -| [showTimeoutError](./kibana-plugin-plugins-data-public.searchinterceptor.showtimeouterror.md) | | ((e: Error) => void) & import("lodash").Cancelable | | ## Methods | Method | Modifiers | Description | | --- | --- | --- | +| [getTimeoutMode()](./kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md) | | | +| [handleSearchError(e, request, timeoutSignal, appAbortSignal)](./kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md) | | | | [search(request, options)](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | Searches using the given search method. Overrides the AbortSignal with one that will abort either when cancelPending is called, when the request times out, or when the original AbortSignal is aborted. Updates pendingCount$ when the request is started/finalized. | +| [showError(e)](./kibana-plugin-plugins-data-public.searchinterceptor.showerror.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.search.md index 1a71b5808f485..672ff5065c456 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.search.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.search.md @@ -9,17 +9,19 @@ Searches using the given `search` method. Overrides the `AbortSignal` with one t Signature: ```typescript -search(request: IEsSearchRequest, options?: ISearchOptions): Observable; +search(request: IKibanaSearchRequest, options?: ISearchOptions): Observable; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| request | IEsSearchRequest | | +| request | IKibanaSearchRequest | | | options | ISearchOptions | | Returns: `Observable` +`Observalbe` emitting the search response or an error. + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showtimeouterror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showerror.md similarity index 53% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showtimeouterror.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showerror.md index 91ecb2821acbf..92e851c783dd0 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showtimeouterror.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showerror.md @@ -1,11 +1,22 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [showTimeoutError](./kibana-plugin-plugins-data-public.searchinterceptor.showtimeouterror.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [showError](./kibana-plugin-plugins-data-public.searchinterceptor.showerror.md) -## SearchInterceptor.showTimeoutError property +## SearchInterceptor.showError() method Signature: ```typescript -protected showTimeoutError: ((e: Error) => void) & import("lodash").Cancelable; +showError(e: Error): void; ``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| e | Error | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md new file mode 100644 index 0000000000000..1c6370c7d0356 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md) > [(constructor)](./kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md) + +## SearchTimeoutError.(constructor) + +Constructs a new instance of the `SearchTimeoutError` class + +Signature: + +```typescript +constructor(err: Error, mode: TimeoutErrorMode); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| err | Error | | +| mode | TimeoutErrorMode | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.geterrormessage.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.geterrormessage.md new file mode 100644 index 0000000000000..58ef953c9d7db --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.geterrormessage.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md) > [getErrorMessage](./kibana-plugin-plugins-data-public.searchtimeouterror.geterrormessage.md) + +## SearchTimeoutError.getErrorMessage() method + +Signature: + +```typescript +getErrorMessage(application: ApplicationStart): JSX.Element; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| application | ApplicationStart | | + +Returns: + +`JSX.Element` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.md new file mode 100644 index 0000000000000..5c0bec04dcfbc --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.md @@ -0,0 +1,32 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md) + +## SearchTimeoutError class + +Request Failure - When an entire multi request fails + +Signature: + +```typescript +export declare class SearchTimeoutError extends KbnError +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(err, mode)](./kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md) | | Constructs a new instance of the SearchTimeoutError class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [mode](./kibana-plugin-plugins-data-public.searchtimeouterror.mode.md) | | TimeoutErrorMode | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [getErrorMessage(application)](./kibana-plugin-plugins-data-public.searchtimeouterror.geterrormessage.md) | | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.mode.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.mode.md new file mode 100644 index 0000000000000..d534a73eca2ec --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.mode.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md) > [mode](./kibana-plugin-plugins-data-public.searchtimeouterror.mode.md) + +## SearchTimeoutError.mode property + +Signature: + +```typescript +mode: TimeoutErrorMode; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timeouterrormode.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timeouterrormode.md new file mode 100644 index 0000000000000..8ad63e2c1e9b4 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timeouterrormode.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [TimeoutErrorMode](./kibana-plugin-plugins-data-public.timeouterrormode.md) + +## TimeoutErrorMode enum + +Signature: + +```typescript +export declare enum TimeoutErrorMode +``` + +## Enumeration Members + +| Member | Value | Description | +| --- | --- | --- | +| CHANGE | 2 | | +| CONTACT | 1 | | +| UPGRADE | 0 | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md index 6d206e88b5b56..a86fea3106225 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md @@ -9,7 +9,7 @@ Add scripted field to field list Signature: ```typescript -addScriptedField(name: string, script: string, fieldType?: string, lang?: string): Promise; +addScriptedField(name: string, script: string, fieldType?: string): Promise; ``` ## Parameters @@ -19,7 +19,6 @@ addScriptedField(name: string, script: string, fieldType?: string, lang?: string | name | string | | | script | string | | | fieldType | string | | -| lang | string | | Returns: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimebasedwildcard.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimebasedwildcard.md deleted file mode 100644 index 7ef5e8318040a..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimebasedwildcard.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [isTimeBasedWildcard](./kibana-plugin-plugins-data-server.indexpattern.istimebasedwildcard.md) - -## IndexPattern.isTimeBasedWildcard() method - -Signature: - -```typescript -isTimeBasedWildcard(): boolean; -``` -Returns: - -`boolean` - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md index d877854444a09..e5a9e7d8f9f93 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md @@ -41,7 +41,7 @@ export declare class IndexPattern implements IIndexPattern | Method | Modifiers | Description | | --- | --- | --- | -| [addScriptedField(name, script, fieldType, lang)](./kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md) | | Add scripted field to field list | +| [addScriptedField(name, script, fieldType)](./kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md) | | Add scripted field to field list | | [getAggregationRestrictions()](./kibana-plugin-plugins-data-server.indexpattern.getaggregationrestrictions.md) | | | | [getAsSavedObjectBody()](./kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md) | | Returns index pattern as saved object body for saving | | [getComputedFields()](./kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md) | | | @@ -52,7 +52,6 @@ export declare class IndexPattern implements IIndexPattern | [getSourceFiltering()](./kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md) | | Get the source filtering configuration for that index. | | [getTimeField()](./kibana-plugin-plugins-data-server.indexpattern.gettimefield.md) | | | | [isTimeBased()](./kibana-plugin-plugins-data-server.indexpattern.istimebased.md) | | | -| [isTimeBasedWildcard()](./kibana-plugin-plugins-data-server.indexpattern.istimebasedwildcard.md) | | | | [isTimeNanosBased()](./kibana-plugin-plugins-data-server.indexpattern.istimenanosbased.md) | | | | [popularizeField(fieldName, unit)](./kibana-plugin-plugins-data-server.indexpattern.popularizefield.md) | | | | [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md) | | Remove scripted field from field list | diff --git a/docs/discover/context.asciidoc b/docs/discover/context.asciidoc index 17ed78a163571..e26c91bfef075 100644 --- a/docs/discover/context.asciidoc +++ b/docs/discover/context.asciidoc @@ -16,7 +16,7 @@ The anchor document is highlighted in blue. [role="screenshot"] -image::images/Discover-ContextView.png[Context View] +image::images/Discover-ContextView.png[Image showing context view feature, with anchor documents highlighted in blue] [float] [[filter-context]] diff --git a/docs/discover/document-data.asciidoc b/docs/discover/document-data.asciidoc index ee130e8405483..dd245e4b4558f 100644 --- a/docs/discover/document-data.asciidoc +++ b/docs/discover/document-data.asciidoc @@ -44,7 +44,7 @@ immediately before and after your event. share the link for direct access to a particular document. [role="screenshot"] -image::images/Expanded-Document.png[] +image::images/Expanded-Document.png[Image showing expanded view, with JSON and table viewing options] [float] diff --git a/docs/discover/field-filter.asciidoc b/docs/discover/field-filter.asciidoc index 949cab2c2f976..0c521b401e4b8 100644 --- a/docs/discover/field-filter.asciidoc +++ b/docs/discover/field-filter.asciidoc @@ -19,7 +19,7 @@ the field, the top 5 values for the field, and the percentage of documents that contain each value. + [role="screenshot"] -image::images/filter-field.png[height=317] +image::images/filter-field.png[Picture showing top 5 values for each field, and correspnding percentage of documents that contain each value] . Use the image:images/PositiveFilter.jpg[Positive Filter] icon to show only documents that contain that value, diff --git a/docs/discover/search.asciidoc b/docs/discover/search.asciidoc index da58382deb89a..ee1e1526f9d6f 100644 --- a/docs/discover/search.asciidoc +++ b/docs/discover/search.asciidoc @@ -28,7 +28,7 @@ configure a refresh interval to periodically resubmit your searches to retrieve the latest results. [role="screenshot"] -image::images/autorefresh-interval.png[] +image::images/autorefresh-interval.png[Image showing what refresh interval option looks like. The configurable time interval is located in the dropdown] You can also manually refresh the search results by clicking the *Refresh* button. diff --git a/docs/discover/set-time-filter.asciidoc b/docs/discover/set-time-filter.asciidoc index a5b81b0fa461c..93fdf9ffd695a 100644 --- a/docs/discover/set-time-filter.asciidoc +++ b/docs/discover/set-time-filter.asciidoc @@ -14,7 +14,7 @@ range in the histogram. Use the time filter to change the time range. By default, the time filter is set to the last 15 minutes. -. Click image:images/time-filter-calendar.png[]. +. Click image:images/time-filter-calendar.png[Calendar icon]. . Choose one of the following: @@ -53,4 +53,4 @@ when you hover over a valid start point. * Click the dropdown, then select an interval. [role="screenshot"] -image::images/Histogram-Time.png[Time range selector in Histogram] +image::images/Histogram-Time.png[Time range selector in Histogram dropdown] diff --git a/docs/discover/viewing-field-stats.asciidoc b/docs/discover/viewing-field-stats.asciidoc index 5ada5839fd344..5c46177347530 100644 --- a/docs/discover/viewing-field-stats.asciidoc +++ b/docs/discover/viewing-field-stats.asciidoc @@ -11,4 +11,4 @@ they are available in the side bar if you uncheck "Hide missing fields". To view field data statistics, click the name of a field in the fields list. -image:images/filter-field.png[Field Statistics,height=317] +image:images/filter-field.png[Fields list that displays the top five search results] diff --git a/docs/getting-started/tutorial-define-index.asciidoc b/docs/getting-started/tutorial-define-index.asciidoc index cb3f6c9ff0c9b..215952c2d3595 100644 --- a/docs/getting-started/tutorial-define-index.asciidoc +++ b/docs/getting-started/tutorial-define-index.asciidoc @@ -24,7 +24,7 @@ index named `shakespeare,` and the accounts data set, which has an index named . In the *Index pattern name* field, enter `shakes*`. + [role="screenshot"] -image::images/tutorial-pattern-1.png[shakes* index patterns] +image::images/tutorial-pattern-1.png[Image showing how to enter shakes* in Index Pattern Name field] . Click *Next step*. @@ -45,7 +45,7 @@ contains the time series data. . From the *Time field* dropdown, select *@timestamp, then click *Create index pattern*. + [role="screenshot"] -image::images/tutorial_index_patterns.png[All tutorial index patterns] +image::images/tutorial_index_patterns.png[Image showing how to create an index pattern] NOTE: When you define an index pattern, the indices that match that pattern must exist in Elasticsearch and they must contain data. To check if the indices are @@ -54,4 +54,3 @@ available, open the menu, go to *Dev Tools > Console*, then enter `GET _cat/indi For Windows, run `Invoke-RestMethod -Uri "http://localhost:9200/_cat/indices"` in Powershell. - diff --git a/docs/getting-started/tutorial-discovering.asciidoc b/docs/getting-started/tutorial-discovering.asciidoc index ec07a74b8ac0d..99a07acf98791 100644 --- a/docs/getting-started/tutorial-discovering.asciidoc +++ b/docs/getting-started/tutorial-discovering.asciidoc @@ -21,7 +21,7 @@ The search returns all account numbers between zero and 99 with balances in excess of 47,500. Results appear for account numbers 8, 32, 78, 85, and 97. + [role="screenshot"] -image::images/tutorial-discover-2.png[] +image::images/tutorial-discover-2.png[Image showing the search results for account numbers between zero and 99, with balances in excess of 47,500] + . Hover over the list of *Available fields*, then click *Add* next to each field you want include in the table. @@ -30,6 +30,6 @@ For example, when you add the `account_number` field, the display changes to a l account numbers. + [role="screenshot"] -image::images/tutorial-discover-3.png[] +image::images/tutorial-discover-3.png[Image showing a dropdown with five account numbers, which match the previous query for account balance] Now that you know what your documents contain, it's time to gain insight into your data with visualizations. diff --git a/docs/getting-started/tutorial-visualizing.asciidoc b/docs/getting-started/tutorial-visualizing.asciidoc index 33a7035160247..a53c8cb6bc23d 100644 --- a/docs/getting-started/tutorial-visualizing.asciidoc +++ b/docs/getting-started/tutorial-visualizing.asciidoc @@ -16,7 +16,7 @@ To visualize the Shakespeare data and compare the number of speaking parts in th . Click *Create new*, then click *Lens* on the *New Visualization* window. + [role="screenshot"] -image::images/tutorial-visualize-wizard-step-1.png[Bar chart] +image::images/tutorial-visualize-wizard-step-1.png[Image showing different options for your new visualization] . Make sure the index pattern is *shakes*. @@ -39,7 +39,7 @@ image::images/tutorial-visualize-wizard-step-1.png[Bar chart] .. In the *Label* field, enter `Speaking Parts`. + [role="screenshot"] -image::images/tutorial-visualize-bar-1.5.png[Bar chart] +image::images/tutorial-visualize-bar-1.5.png[Bar chart showing the speaking parts data] . *Save* the chart with the name `Bar Example`. + @@ -85,7 +85,7 @@ Since the default search matches all documents, the pie contains a single slice. The pie chart displays the proportion of the 1,000 accounts that fall into each of the ranges. + [role="screenshot"] -image::images/tutorial-visualize-pie-2.png[Pie chart] +image::images/tutorial-visualize-pie-2.png[Pie chart displaying accounts that fall into each of the ranges, scaled to 1000 accounts] . Add another bucket aggregation that displays the ages of the account holders. @@ -99,7 +99,7 @@ The break down of the ages of the account holders are displayed in a ring around the balance ranges. + [role="screenshot"] -image::images/tutorial-visualize-pie-3.png[Final pie chart] +image::images/tutorial-visualize-pie-3.png[Final pie chart showing all of the changes] . Click *Save*, then enter `Pie Example` in the *Title* field. @@ -119,7 +119,7 @@ To visualize geographic information in the log file data, use <>. .. Set the *End date* to `May 20, 2015 @ 12:00:00.000`. + [role="screenshot"] -image::images/gs_maps_time_filter.png[Time filter for Maps tutorial] +image::images/gs_maps_time_filter.png[Image showing the time filter for Maps tutorial] .. Click *Update* @@ -140,7 +140,7 @@ image::images/gs_maps_time_filter.png[Time filter for Maps tutorial] .. From the *Border color* dropdown, select *#FFF*, then click *Save & close*. + [role="screenshot"] -image::images/tutorial-visualize-map-2.png[Map] +image::images/tutorial-visualize-map-2.png[Example of a map visualization] . Click *Save*, then enter `Map Example` in the *Title* field. @@ -170,12 +170,12 @@ The Markdown widget uses **markdown** syntax. The Markdown renders in the preview pane. + [role="screenshot"] -image::images/tutorial-visualize-md-2.png[] +image::images/tutorial-visualize-md-2.png[Image showing example markdown editing field] . Click *Save*, then enter `Markdown Example` in the *Title* field. [role="screenshot"] -image::images/tutorial-dashboard.png[] +image::images/tutorial-dashboard.png[Final visualization with bar chart, pie chart, map, and markdown text field] [float] === Next steps diff --git a/examples/alerting_example/public/alert_types/index.ts b/examples/alerting_example/public/alert_types/index.ts deleted file mode 100644 index db9f855b573e8..0000000000000 --- a/examples/alerting_example/public/alert_types/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { registerNavigation as registerPeopleInSpaceNavigation } from './astros'; -import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; -import { SanitizedAlert } from '../../../../x-pack/plugins/alerts/common'; -import { PluginSetupContract as AlertingSetup } from '../../../../x-pack/plugins/alerts/public'; - -export function registerNavigation(alerts: AlertingSetup) { - // register default navigation - alerts.registerDefaultNavigation( - ALERTING_EXAMPLE_APP_ID, - (alert: SanitizedAlert) => `/alert/${alert.id}` - ); - - registerPeopleInSpaceNavigation(alerts); -} diff --git a/examples/alerting_example/public/index.ts b/examples/alerting_example/public/index.ts deleted file mode 100644 index 4a2bfc79903c3..0000000000000 --- a/examples/alerting_example/public/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { AlertingExamplePlugin } from './plugin'; - -export const plugin = () => new AlertingExamplePlugin(); diff --git a/examples/alerting_example/server/alert_types/always_firing.ts b/examples/alerting_example/server/alert_types/always_firing.ts deleted file mode 100644 index b89e5da089336..0000000000000 --- a/examples/alerting_example/server/alert_types/always_firing.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import uuid from 'uuid'; -import { range } from 'lodash'; -import { AlertType } from '../../../../x-pack/plugins/alerts/server'; -import { DEFAULT_INSTANCES_TO_GENERATE, ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; - -export const alertType: AlertType = { - id: 'example.always-firing', - name: 'Always firing', - actionGroups: [{ id: 'default', name: 'default' }], - defaultActionGroupId: 'default', - async executor({ services, params: { instances = DEFAULT_INSTANCES_TO_GENERATE }, state }) { - const count = (state.count ?? 0) + 1; - - range(instances) - .map(() => ({ id: uuid.v4() })) - .forEach((instance: { id: string }) => { - services - .alertInstanceFactory(instance.id) - .replaceState({ triggerdOnCycle: count }) - .scheduleActions('default'); - }); - - return { - count, - }; - }, - producer: ALERTING_EXAMPLE_APP_ID, -}; diff --git a/examples/alerting_example/server/index.ts b/examples/alerting_example/server/index.ts deleted file mode 100644 index 32e9b181ebb54..0000000000000 --- a/examples/alerting_example/server/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { PluginInitializer } from 'kibana/server'; -import { AlertingExamplePlugin } from './plugin'; - -export const plugin: PluginInitializer = () => new AlertingExamplePlugin(); diff --git a/examples/search_examples/server/my_strategy.ts b/examples/search_examples/server/my_strategy.ts index 1f59d0a5d8f3a..169982544e6e8 100644 --- a/examples/search_examples/server/my_strategy.ts +++ b/examples/search_examples/server/my_strategy.ts @@ -25,7 +25,7 @@ export const mySearchStrategyProvider = ( ): ISearchStrategy => { const es = data.search.getSearchStrategy('es'); return { - search: async (context, request, options) => { + search: async (context, request, options): Promise => { const esSearchRes = await es.search(context, request, options); return { ...esSearchRes, diff --git a/package.json b/package.json index 505fac1df935e..9b1735f6f7e5e 100644 --- a/package.json +++ b/package.json @@ -155,7 +155,7 @@ "check-disk-space": "^2.1.0", "chokidar": "3.2.1", "color": "1.0.3", - "commander": "3.0.2", + "commander": "^3.0.2", "core-js": "^3.6.4", "cypress-promise": "^1.1.0", "deep-freeze-strict": "^1.1.1", @@ -184,12 +184,12 @@ "json-stable-stringify": "^1.0.1", "json-stringify-safe": "5.0.1", "lodash": "^4.17.20", - "lru-cache": "4.1.5", + "lru-cache": "^4.1.5", "minimatch": "^3.0.4", "moment": "^2.24.0", "moment-timezone": "^0.5.27", - "mustache": "2.3.2", - "node-fetch": "2.6.1", + "mustache": "^2.3.2", + "node-fetch": "^2.6.1", "node-forge": "^0.10.0", "opn": "^5.5.0", "oppsy": "^2.0.0", @@ -212,7 +212,7 @@ "rison-node": "1.0.2", "rxjs": "^6.5.5", "seedrandom": "^3.0.5", - "semver": "^5.5.0", + "semver": "^5.7.0", "style-it": "^2.1.3", "symbol-observable": "^1.2.0", "tar": "4.4.13", @@ -223,7 +223,7 @@ "uuid": "3.3.2", "vision": "^5.3.3", "whatwg-fetch": "^3.0.0", - "yauzl": "2.10.0" + "yauzl": "^2.10.0" }, "devDependencies": { "@babel/parser": "^7.11.2", @@ -279,7 +279,7 @@ "@types/flot": "^0.0.31", "@types/getopts": "^2.0.1", "@types/getos": "^3.0.0", - "@types/glob": "^7.1.1", + "@types/glob": "^7.1.2", "@types/globby": "^8.0.0", "@types/graphql": "^0.13.2", "@types/h2o2": "^8.1.1", @@ -312,7 +312,7 @@ "@types/normalize-path": "^3.0.0", "@types/opn": "^5.1.0", "@types/pegjs": "^0.10.1", - "@types/pngjs": "^3.3.2", + "@types/pngjs": "^3.4.0", "@types/podium": "^1.0.0", "@types/prop-types": "^15.7.3", "@types/reach__router": "^1.2.6", @@ -393,7 +393,7 @@ "fetch-mock": "^7.3.9", "fp-ts": "^2.3.1", "geckodriver": "^1.20.0", - "getopts": "^2.2.4", + "getopts": "^2.2.5", "grunt": "1.0.4", "grunt-available-tasks": "^0.6.3", "grunt-cli": "^1.2.0", @@ -439,7 +439,7 @@ "multistream": "^2.1.1", "murmurhash3js": "3.0.1", "mutation-observer": "^1.0.3", - "ngreact": "0.5.1", + "ngreact": "^0.5.1", "nock": "12.0.3", "normalize-path": "^3.0.0", "nyc": "^15.0.1", diff --git a/packages/kbn-ace/package.json b/packages/kbn-ace/package.json index cf74d745f4cae..6f1cee764adf1 100644 --- a/packages/kbn-ace/package.json +++ b/packages/kbn-ace/package.json @@ -14,7 +14,7 @@ "devDependencies": { "@kbn/dev-utils": "1.0.0", "@kbn/babel-preset": "1.0.0", - "raw-loader": "3.1.0", + "raw-loader": "^3.1.0", "typescript": "4.0.2" } } diff --git a/packages/kbn-config/package.json b/packages/kbn-config/package.json index 2d9dbc3b7ab8f..062520f47f0f9 100644 --- a/packages/kbn-config/package.json +++ b/packages/kbn-config/package.json @@ -25,6 +25,6 @@ }, "devDependencies": { "typescript": "4.0.2", - "tsd": "^0.7.4" + "tsd": "^0.13.1" } } diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json index a3fe8178822aa..a85f5924f0ea2 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -12,7 +12,7 @@ "dependencies": { "@babel/core": "^7.11.1", "@kbn/utils": "1.0.0", - "axios": "^0.19.0", + "axios": "^0.19.2", "chalk": "^4.1.0", "cheerio": "0.22.0", "dedent": "^0.7.0", diff --git a/packages/kbn-es/package.json b/packages/kbn-es/package.json index 52ef3fe05e751..40d34c5d710bb 100644 --- a/packages/kbn-es/package.json +++ b/packages/kbn-es/package.json @@ -1,23 +1,32 @@ { "name": "@kbn/es", - "main": "./src/index.js", + "main": "./target/index.js", "version": "1.0.0", "license": "Apache-2.0", "private": true, + "scripts": { + "kbn:bootstrap": "node scripts/build", + "kbn:watch": "node scripts/build --watch" + }, "dependencies": { "@elastic/elasticsearch": "7.9.0-rc.1", "@kbn/dev-utils": "1.0.0", - "abort-controller": "^2.0.3", + "abort-controller": "^3.0.0", "chalk": "^4.1.0", "dedent": "^0.7.0", "del": "^5.1.0", "execa": "^4.0.2", - "getopts": "^2.2.4", + "getopts": "^2.2.5", "glob": "^7.1.2", "node-fetch": "^2.6.1", - "simple-git": "^1.91.0", + "simple-git": "1.116.0", "tar-fs": "^2.1.0", "tree-kill": "^1.2.2", "yauzl": "^2.10.0" + }, + "devDependencies": { + "@kbn/babel-preset": "1.0.0", + "@babel/cli": "^7.10.5", + "del": "^5.1.0" } } diff --git a/packages/kbn-es/scripts/build.js b/packages/kbn-es/scripts/build.js new file mode 100644 index 0000000000000..50aad665c920b --- /dev/null +++ b/packages/kbn-es/scripts/build.js @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const { resolve } = require('path'); + +const del = require('del'); +const { run, withProcRunner } = require('@kbn/dev-utils'); + +const ROOT_DIR = resolve(__dirname, '..'); +const BUILD_DIR = resolve(ROOT_DIR, 'target'); + +run( + async ({ log, flags }) => { + await withProcRunner(log, async (proc) => { + log.info('Deleting old output'); + await del(BUILD_DIR); + + const cwd = ROOT_DIR; + + log.info(`Starting babel${flags.watch ? ' in watch mode' : ''}`); + await proc.run(`babel`, { + cmd: 'babel', + args: [ + 'src', + '--no-babelrc', + '--presets', + require.resolve('@kbn/babel-preset/node_preset'), + '--extensions', + '.ts,.js', + '--copy-files', + '--out-dir', + BUILD_DIR, + ...(flags.watch ? ['--watch'] : ['--quiet']), + ...(!flags['source-maps'] || !!process.env.CODE_COVERAGE + ? [] + : ['--source-maps', 'inline']), + ], + wait: true, + cwd, + }); + + log.success('Complete'); + }); + }, + { + description: 'Simple build tool for @kbn/es package', + flags: { + boolean: ['watch', 'source-maps'], + help: ` + --watch Run in watch mode + --source-maps Include sourcemaps + `, + }, + } +); diff --git a/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js b/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js index b860664443d1a..27e73e6c204e8 100644 --- a/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js +++ b/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js @@ -25,65 +25,67 @@ const { exitCode, start, ssl } = JSON.parse(process.argv[2]); const { createServer } = ssl ? require('https') : require('http'); const { ES_KEY_PATH, ES_CERT_PATH } = require('@kbn/dev-utils'); -process.exitCode = exitCode; +(function main() { + process.exitCode = exitCode; -if (!start) { - return; -} + if (!start) { + return; + } + + let serverUrl; + const server = createServer( + { + // Note: the integration uses the ES_P12_PATH, but that keystore contains + // the same key/cert as ES_KEY_PATH and ES_CERT_PATH + key: ssl ? fs.readFileSync(ES_KEY_PATH) : undefined, + cert: ssl ? fs.readFileSync(ES_CERT_PATH) : undefined, + }, + (req, res) => { + const url = new URL(req.url, serverUrl); + const send = (code, body) => { + res.writeHead(code, { 'content-type': 'application/json' }); + res.end(JSON.stringify(body)); + }; -let serverUrl; -const server = createServer( - { - // Note: the integration uses the ES_P12_PATH, but that keystore contains - // the same key/cert as ES_KEY_PATH and ES_CERT_PATH - key: ssl ? fs.readFileSync(ES_KEY_PATH) : undefined, - cert: ssl ? fs.readFileSync(ES_CERT_PATH) : undefined, - }, - (req, res) => { - const url = new URL(req.url, serverUrl); - const send = (code, body) => { - res.writeHead(code, { 'content-type': 'application/json' }); - res.end(JSON.stringify(body)); - }; + if (url.pathname === '/_xpack') { + return send(400, { + error: { + reason: 'foo bar', + }, + }); + } - if (url.pathname === '/_xpack') { - return send(400, { + return send(404, { error: { - reason: 'foo bar', + reason: 'not found', }, }); } + ); - return send(404, { - error: { - reason: 'not found', - }, - }); - } -); - -// setup server auto close after 1 second of silence -let serverCloseTimer; -const delayServerClose = () => { - clearTimeout(serverCloseTimer); - serverCloseTimer = setTimeout(() => server.close(), 1000); -}; -server.on('request', delayServerClose); -server.on('listening', delayServerClose); + // setup server auto close after 1 second of silence + let serverCloseTimer; + const delayServerClose = () => { + clearTimeout(serverCloseTimer); + serverCloseTimer = setTimeout(() => server.close(), 1000); + }; + server.on('request', delayServerClose); + server.on('listening', delayServerClose); -server.listen(0, '127.0.0.1', function () { - const { port, address: hostname } = server.address(); - serverUrl = new URL( - formatUrl({ - protocol: 'http:', - port, - hostname, - }) - ); + server.listen(0, '127.0.0.1', function () { + const { port, address: hostname } = server.address(); + serverUrl = new URL( + formatUrl({ + protocol: 'http:', + port, + hostname, + }) + ); - console.log( - `[o.e.h.AbstractHttpServerTransport] [computer] publish_address {127.0.0.1:${port}}, bound_addresses {[::1]:${port}}, {127.0.0.1:${port}}` - ); + console.log( + `[o.e.h.AbstractHttpServerTransport] [computer] publish_address {127.0.0.1:${port}}, bound_addresses {[::1]:${port}}, {127.0.0.1:${port}}` + ); - console.log('started'); -}); + console.log('started'); + }); +})(); diff --git a/packages/kbn-eslint-import-resolver-kibana/package.json b/packages/kbn-eslint-import-resolver-kibana/package.json index 332f7e8a20cc2..223c73e97908e 100755 --- a/packages/kbn-eslint-import-resolver-kibana/package.json +++ b/packages/kbn-eslint-import-resolver-kibana/package.json @@ -13,7 +13,7 @@ "debug": "^2.6.9", "eslint-import-resolver-node": "0.3.2", "eslint-import-resolver-webpack": "0.11.1", - "glob-all": "^3.1.0", + "glob-all": "^3.2.1", "lru-cache": "^4.1.5", "resolve": "^1.7.1", "webpack": "^4.41.5" diff --git a/packages/kbn-i18n/package.json b/packages/kbn-i18n/package.json index eccdff9060cbe..9e0ec50fb838e 100644 --- a/packages/kbn-i18n/package.json +++ b/packages/kbn-i18n/package.json @@ -19,7 +19,7 @@ "@types/intl-relativeformat": "^2.1.0", "@types/react-intl": "^2.3.15", "del": "^5.1.0", - "getopts": "^2.2.4", + "getopts": "^2.2.5", "supports-color": "^7.0.0", "typescript": "4.0.2" }, diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json index aef63229ebe96..430dac6cb2e00 100644 --- a/packages/kbn-interpreter/package.json +++ b/packages/kbn-interpreter/package.json @@ -11,7 +11,7 @@ "dependencies": { "@babel/runtime": "^7.11.2", "@kbn/i18n": "1.0.0", - "lodash": "^4.17.15", + "lodash": "^4.17.20", "uuid": "3.3.2" }, "devDependencies": { @@ -25,12 +25,12 @@ "copy-webpack-plugin": "^6.0.2", "css-loader": "^3.4.2", "del": "^5.1.0", - "getopts": "^2.2.4", + "getopts": "^2.2.5", "pegjs": "0.10.0", "sass-loader": "^8.0.2", "style-loader": "^1.1.3", "supports-color": "^7.0.0", - "url-loader": "2.2.0", + "url-loader": "^2.2.0", "webpack": "^4.41.5", "webpack-cli": "^3.3.10" } diff --git a/packages/kbn-monaco/package.json b/packages/kbn-monaco/package.json index ca133010fe230..fcea80c9b7110 100644 --- a/packages/kbn-monaco/package.json +++ b/packages/kbn-monaco/package.json @@ -18,7 +18,7 @@ "babel-loader": "^8.0.6", "css-loader": "^3.4.2", "del": "^5.1.0", - "raw-loader": "3.1.0", + "raw-loader": "^3.1.0", "supports-color": "^7.0.0", "typescript": "4.0.2", "webpack": "^4.41.5", diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index b80d1365659dd..f90fcaec79fe0 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -27,7 +27,7 @@ "jest-diff": "^25.5.0", "json-stable-stringify": "^1.0.1", "loader-utils": "^1.2.3", - "node-sass": "^4.13.0", + "node-sass": "^4.13.1", "normalize-path": "^3.0.0", "postcss": "^7.0.32", "postcss-loader": "^3.0.0", diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index 45598ff8831b0..b1ab1ebfe49f2 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -161,7 +161,8 @@ export class OptimizerConfig { Path.resolve(repoRoot, 'src/plugins'), ...(oss ? [] : [Path.resolve(repoRoot, 'x-pack/plugins')]), Path.resolve(repoRoot, 'plugins'), - ...(examples ? [Path.resolve('examples'), Path.resolve('x-pack/examples')] : []), + ...(examples ? [Path.resolve('examples')] : []), + ...(examples && !oss ? [Path.resolve('x-pack/examples')] : []), Path.resolve(repoRoot, '../kibana-extra'), ]; if (!pluginScanDirs.every((p) => Path.isAbsolute(p))) { diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 9f2c5654a8bd4..2edf1c999888e 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -216,7 +216,7 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: }, resolve: { - extensions: ['.js', '.ts', '.tsx', 'json'], + extensions: ['.js', '.ts', '.tsx', '.json'], mainFields: ['browser', 'main'], alias: { tinymath: require.resolve('tinymath/lib/tinymath.es5.js'), diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json index f292387c12521..65b44b6965048 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -25,7 +25,7 @@ "devDependencies": { "@types/extract-zip": "^1.6.2", "@types/gulp-zip": "^4.0.1", - "@types/inquirer": "^6.5.0", + "@types/inquirer": "^7.3.1", "extract-zip": "^2.0.1", "typescript": "4.0.2" } diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index 1fb94e4c92ce1..c2f9236d9e798 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -19,8 +19,8 @@ "@types/cpy": "^5.1.0", "@types/dedent": "^0.7.0", "@types/getopts": "^2.0.1", - "@types/glob": "^5.0.35", - "@types/globby": "^6.1.0", + "@types/glob": "^7.1.2", + "@types/globby": "^8.0.0", "@types/has-ansi": "^3.0.0", "@types/lodash": "^4.14.159", "@types/log-symbols": "^2.0.0", @@ -41,12 +41,12 @@ "dedent": "^0.7.0", "del": "^5.1.0", "execa": "^4.0.2", - "getopts": "^2.2.4", + "getopts": "^2.2.5", "glob": "^7.1.2", "globby": "^8.0.1", "has-ansi": "^3.0.0", "is-path-inside": "^3.0.2", - "lodash": "^4.17.15", + "lodash": "^4.17.20", "log-symbols": "^2.2.0", "multimatch": "^4.0.0", "ncp": "^2.0.0", diff --git a/packages/kbn-spec-to-console/package.json b/packages/kbn-spec-to-console/package.json index 63ce93ac54f46..0c80c949c1d11 100644 --- a/packages/kbn-spec-to-console/package.json +++ b/packages/kbn-spec-to-console/package.json @@ -21,7 +21,7 @@ "prettier": "^2.1.1" }, "dependencies": { - "commander": "^3.0.0", + "commander": "^3.0.2", "glob": "^7.1.2" } -} \ No newline at end of file +} diff --git a/packages/kbn-std/package.json b/packages/kbn-std/package.json index 2cc9fd72082be..a931dd3f3154d 100644 --- a/packages/kbn-std/package.json +++ b/packages/kbn-std/package.json @@ -15,6 +15,6 @@ }, "dependencies": { "@kbn/utility-types": "1.0.0", - "lodash": "^4.17.15" + "lodash": "^4.17.20" } } diff --git a/packages/kbn-storybook/package.json b/packages/kbn-storybook/package.json index 5271ddb96d842..30ab0c701b295 100644 --- a/packages/kbn-storybook/package.json +++ b/packages/kbn-storybook/package.json @@ -22,10 +22,10 @@ "jest-specific-snapshot": "2.0.0", "jest-styled-components": "^7.0.2", "mkdirp": "0.5.1", - "mini-css-extract-plugin": "0.7.0", - "normalize-path": "3.0.0", - "react-docgen-typescript-loader": "3.1.0", - "rxjs": "6.5.5", + "mini-css-extract-plugin": "0.8.0", + "normalize-path": "^3.0.0", + "react-docgen-typescript-loader": "^3.1.1", + "rxjs": "^6.5.5", "serve-static": "1.14.1", "styled-components": "^5.1.0", "webpack": "^4.41.5" diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json b/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json index 2e69d3625d7ff..51e5df9bf7dc0 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json @@ -35,16 +35,19 @@ } }, "my_array": { - "properties": { - "total": { - "type": "number" - }, - "type": { - "type": "boolean" + "type": "array", + "items": { + "properties": { + "total": { + "type": "number" + }, + "type": { + "type": "boolean" + } } } }, - "my_str_array": { "type": "keyword" } + "my_str_array": { "type": "array", "items": { "type": "keyword" } } } } } diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts index 54983278726eb..acf984b7d10ee 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts @@ -55,12 +55,15 @@ export const parsedWorkingCollector: ParsedUsageCollection = [ }, }, my_array: { - total: { - type: 'number', + type: 'array', + items: { + total: { + type: 'number', + }, + type: { type: 'boolean' }, }, - type: { type: 'boolean' }, }, - my_str_array: { type: 'keyword' }, + my_str_array: { type: 'array', items: { type: 'keyword' } }, }, }, fetch: { @@ -91,18 +94,22 @@ export const parsedWorkingCollector: ParsedUsageCollection = [ }, }, my_array: { - total: { - kind: SyntaxKind.NumberKeyword, - type: 'NumberKeyword', - }, - type: { - kind: SyntaxKind.BooleanKeyword, - type: 'BooleanKeyword', + items: { + total: { + kind: SyntaxKind.NumberKeyword, + type: 'NumberKeyword', + }, + type: { + kind: SyntaxKind.BooleanKeyword, + type: 'BooleanKeyword', + }, }, }, my_str_array: { - kind: SyntaxKind.StringKeyword, - type: 'StringKeyword', + items: { + kind: SyntaxKind.StringKeyword, + type: 'StringKeyword', + }, }, }, }, diff --git a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap index 9868a7d31d498..206f573b0af78 100644 --- a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap +++ b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap @@ -153,13 +153,15 @@ Array [ "type": "StringKeyword", }, "my_array": Object { - "total": Object { - "kind": 143, - "type": "NumberKeyword", - }, - "type": Object { - "kind": 131, - "type": "BooleanKeyword", + "items": Object { + "total": Object { + "kind": 143, + "type": "NumberKeyword", + }, + "type": Object { + "kind": 131, + "type": "BooleanKeyword", + }, }, }, "my_index_signature_prop": Object { @@ -183,8 +185,10 @@ Array [ "type": "StringKeyword", }, "my_str_array": Object { - "kind": 146, - "type": "StringKeyword", + "items": Object { + "kind": 146, + "type": "StringKeyword", + }, }, }, "typeName": "Usage", @@ -195,12 +199,15 @@ Array [ "type": "keyword", }, "my_array": Object { - "total": Object { - "type": "number", - }, - "type": Object { - "type": "boolean", + "items": Object { + "total": Object { + "type": "number", + }, + "type": Object { + "type": "boolean", + }, }, + "type": "array", }, "my_index_signature_prop": Object { "avg": Object { @@ -228,7 +235,10 @@ Array [ "type": "text", }, "my_str_array": Object { - "type": "keyword", + "items": Object { + "type": "keyword", + }, + "type": "array", }, }, }, diff --git a/packages/kbn-telemetry-tools/src/tools/manage_schema.ts b/packages/kbn-telemetry-tools/src/tools/manage_schema.ts index d422837140d80..7721492fdb691 100644 --- a/packages/kbn-telemetry-tools/src/tools/manage_schema.ts +++ b/packages/kbn-telemetry-tools/src/tools/manage_schema.ts @@ -28,7 +28,7 @@ export type AllowedSchemaTypes = | 'date' | 'float'; -export function compatibleSchemaTypes(type: AllowedSchemaTypes) { +export function compatibleSchemaTypes(type: AllowedSchemaTypes | 'array') { switch (type) { case 'keyword': case 'text': @@ -40,6 +40,8 @@ export function compatibleSchemaTypes(type: AllowedSchemaTypes) { case 'float': case 'long': return 'number'; + case 'array': + return 'array'; default: throw new Error(`Unknown schema type ${type}`); } @@ -66,10 +68,22 @@ export function isObjectMapping(entity: any) { return false; } +function isArrayMapping(entity: any): entity is { type: 'array'; items: object } { + return typeof entity === 'object' && entity.type === 'array' && typeof entity.items === 'object'; +} + +function getValueMapping(value: any) { + return isObjectMapping(value) ? transformToEsMapping(value) : value; +} + function transformToEsMapping(usageMappingValue: any) { const fieldMapping: any = { properties: {} }; for (const [key, value] of Object.entries(usageMappingValue)) { - fieldMapping.properties[key] = isObjectMapping(value) ? transformToEsMapping(value) : value; + if (isArrayMapping(value)) { + fieldMapping.properties[key] = { ...value, items: getValueMapping(value.items) }; + } else { + fieldMapping.properties[key] = getValueMapping(value); + } } return fieldMapping; } diff --git a/packages/kbn-telemetry-tools/src/tools/serializer.test.ts b/packages/kbn-telemetry-tools/src/tools/serializer.test.ts index 6742117226368..02d663f4d29eb 100644 --- a/packages/kbn-telemetry-tools/src/tools/serializer.test.ts +++ b/packages/kbn-telemetry-tools/src/tools/serializer.test.ts @@ -84,8 +84,8 @@ describe('getDescriptor', () => { expect(descriptor).toEqual({ prop1: { kind: TelemetryKinds.MomentDate, type: 'MomentDate' }, prop2: { kind: TelemetryKinds.MomentDate, type: 'MomentDate' }, - prop3: { kind: TelemetryKinds.MomentDate, type: 'MomentDate' }, - prop4: { kind: TelemetryKinds.Date, type: 'Date' }, + prop3: { items: { kind: TelemetryKinds.MomentDate, type: 'MomentDate' } }, + prop4: { items: { kind: TelemetryKinds.Date, type: 'Date' } }, }); }); diff --git a/packages/kbn-telemetry-tools/src/tools/serializer.ts b/packages/kbn-telemetry-tools/src/tools/serializer.ts index 6fe02e3824ba7..422b298c58374 100644 --- a/packages/kbn-telemetry-tools/src/tools/serializer.ts +++ b/packages/kbn-telemetry-tools/src/tools/serializer.ts @@ -139,7 +139,7 @@ export function getDescriptor(node: ts.Node, program: ts.Program): Descriptor | } if (ts.isArrayTypeNode(node)) { - return getDescriptor(node.elementType, program); + return { items: getDescriptor(node.elementType, program) }; } if (ts.isLiteralTypeNode(node)) { diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index 8a4ff55dcf68f..c616c836d5ff4 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -26,15 +26,14 @@ "dedent": "^0.7.0", "del": "^5.1.0", "exit-hook": "^2.2.0", - "getopts": "^2.2.4", + "getopts": "^2.2.5", "glob": "^7.1.2", "joi": "^13.5.2", - "lodash": "^4.17.15", + "lodash": "^4.17.20", "parse-link-header": "^1.0.1", "rxjs": "^6.5.5", "strip-ansi": "^5.2.0", "tar-fs": "^2.1.0", - "tmp": "^0.1.0", "xml2js": "^0.4.22", "zlib": "^1.0.5" } diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index 363f97522a901..1f916c58e2ecd 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -17,7 +17,7 @@ "dependencies": { "classnames": "2.2.6", "focus-trap-react": "^3.1.1", - "lodash": "^4.17.15", + "lodash": "^4.17.20", "prop-types": "^15.7.2", "react": "^16.12.0", "react-ace": "^5.9.0", @@ -31,7 +31,7 @@ }, "devDependencies": { "@babel/core": "^7.11.1", - "@elastic/eui": "0.0.55", + "@elastic/eui": "29.0.0", "@kbn/babel-preset": "1.0.0", "@kbn/optimizer": "1.0.0", "babel-loader": "^8.0.6", @@ -62,7 +62,7 @@ "react-router": "^3.2.0", "react-router-redux": "^4.0.8", "redux": "3.7.2", - "redux-thunk": "2.2.0", + "redux-thunk": "^2.3.0", "regenerator-runtime": "^0.13.3", "sass-loader": "^8.0.2", "sinon": "^7.4.2", diff --git a/rfcs/text/0013_saved_object_migrations.md b/rfcs/text/0013_saved_object_migrations.md new file mode 100644 index 0000000000000..c5069625cb8a6 --- /dev/null +++ b/rfcs/text/0013_saved_object_migrations.md @@ -0,0 +1,745 @@ +- Start Date: 2020-05-11 +- RFC PR: (leave this empty) +- Kibana Issue: (leave this empty) + +--- +- [1. Summary](#1-summary) +- [2. Motivation](#2-motivation) +- [3. Saved Object Migration Errors](#3-saved-object-migration-errors) +- [4. Design](#4-design) + - [4.0 Assumptions and tradeoffs](#40-assumptions-and-tradeoffs) + - [4.1 Discover and remedy potential failures before any downtime](#41-discover-and-remedy-potential-failures-before-any-downtime) + - [4.2 Automatically retry failed migrations until they succeed](#42-automatically-retry-failed-migrations-until-they-succeed) + - [4.2.1 Idempotent migrations performed without coordination](#421-idempotent-migrations-performed-without-coordination) + - [4.2.1.1 Restrictions](#4211-restrictions) + - [4.2.1.2 Migration algorithm: Cloned index per version](#4212-migration-algorithm-cloned-index-per-version) + - [4.2.1.3 Upgrade and rollback procedure](#4213-upgrade-and-rollback-procedure) + - [4.2.1.4 Handling documents that belong to a disabled plugin](#4214-handling-documents-that-belong-to-a-disabled-plugin) +- [5. Alternatives](#5-alternatives) + - [5.1 Rolling upgrades](#51-rolling-upgrades) + - [5.2 Single node migrations coordinated through a lease/lock](#52-single-node-migrations-coordinated-through-a-leaselock) + - [5.2.1 Migration algorithm](#521-migration-algorithm) + - [5.2.2 Document lock algorithm](#522-document-lock-algorithm) + - [5.2.3 Checking for "weak lease" expiry](#523-checking-for-weak-lease-expiry) + - [5.3 Minimize data loss with mixed Kibana versions during 7.x](#53-minimize-data-loss-with-mixed-kibana-versions-during-7x) + - [5.4 In-place migrations that re-use the same index (8.0)](#54-in-place-migrations-that-re-use-the-same-index-80) + - [5.4.1 Migration algorithm (8.0):](#541-migration-algorithm-80) + - [5.4.2 Minimizing data loss with unsupported upgrade configurations (8.0)](#542-minimizing-data-loss-with-unsupported-upgrade-configurations-80) + - [5.5 Tag objects as “invalid” if their transformation fails](#55-tag-objects-as-invalid-if-their-transformation-fails) +- [6. How we teach this](#6-how-we-teach-this) +- [7. Unresolved questions](#7-unresolved-questions) + +# 1. Summary + +Improve the Saved Object migration algorithm to ensure a smooth Kibana upgrade +procedure. + +# 2. Motivation + +Kibana version upgrades should have a minimal operational impact. To achieve +this, users should be able to rely on: + +1. A predictable downtime window. +2. A small downtime window. + 1. (future) provide a small downtime window on indices with 10k or even + a 100k documents. +3. The ability to discover and remedy potential failures before initiating the + downtime window. +4. Quick roll-back in case of failure. +5. Detailed documentation about the impact of downtime on the features they + are using (e.g. actions, task manager, fleet, reporting). +6. Mixed Kibana versions shouldn’t cause data loss. +7. (stretch goal) Maintain read-only functionality during the downtime window. + +The biggest hurdle to achieving the above is Kibana’s Saved Object migrations. +Migrations aren’t resilient and require manual intervention anytime an error +occurs (see [3. Saved Object Migration +Errors](#3-saved-object-migration-errors)). + +It is impossible to discover these failures before initiating downtime. Errors +often force users to roll-back to a previous version of Kibana or cause hours +of downtime. To retry the migration, users are asked to manually delete a +`.kibana_x` index. If done incorrectly this can lead to data loss, making it a +terrifying experience (restoring from a pre-upgrade snapshot is a safer +alternative but not mentioned in the docs or logs). + +Cloud users don’t have access to Kibana logs to be able to identify and remedy +the cause of the migration failure. Apart from blindly retrying migrations by +restoring a previous snapshot, cloud users are unable to remedy a failed +migration and have to escalate to support which can further delay resolution. + +Taken together, version upgrades are a major operational risk and discourage +users from adopting the latest features. + +# 3. Saved Object Migration Errors + +Any of the following classes of errors could result in a Saved Object +migration failure which requires manual intervention to resolve: + +1. A bug in a plugin’s registered document transformation function causes it + to throw an exception on _valid_ data. +2. _Invalid_ data stored in Elasticsearch causes a plugin’s registered + document transformation function to throw an exception . +3. Failures resulting from an unhealthy Elasticsearch cluster: + 1. Maximum shards open + 2. Too many scroll contexts + 3. `circuit_breaking_exception` (insufficient heap memory) + 4. `process_cluster_event_timeout_exception` for index-aliases, create-index, put-mappings + 5. Read-only indices due to low disk space (hitting the flood_stage watermark) + 6. Re-index failed: search rejected due to missing shards + 7. `TooManyRequests` while doing a `count` of documents requiring a migration + 8. Bulk write failed: primary shard is not active +4. The Kibana process is killed while migrations are in progress. + +# 4. Design +## 4.0 Assumptions and tradeoffs +The proposed design makes several important assumptions and tradeoffs. + +**Background:** + +The 7.x upgrade documentation lists taking an Elasticsearch snapshot as a +required step, but we instruct users to retry migrations and perform rollbacks +by deleting the failed `.kibana_n` index and pointing the `.kibana` alias to +`.kibana_n-1`: + - [Handling errors during saved object +migrations.](https://github.com/elastic/kibana/blob/75444a9f1879c5702f9f2b8ad4a70a3a0e75871d/docs/setup/upgrade/upgrade-migrations.asciidoc#handling-errors-during-saved-object-migrations) + - [Rolling back to a previous version of Kibana.](https://github.com/elastic/kibana/blob/75444a9f1879c5702f9f2b8ad4a70a3a0e75871d/docs/setup/upgrade/upgrade-migrations.asciidoc#rolling-back-to-a-previous-version-of-kib) + - Server logs from failed migrations. + +**Assumptions and tradeoffs:** +1. It is critical to maintain a backup index during 7.x to ensure that anyone + following the existing upgrade / rollback procedures don't end up in a + position where they no longer can recover their data. + 1. This excludes us from introducing in-place migrations to support huge + indices during 7.x. +2. The simplicity of idempotent, coordination-free migrations outweighs the + restrictions this will impose on the kinds of migrations we're able to + support in the future. See (4.2.1) +3. A saved object type (and it's associated migrations) will only ever be + owned by one plugin. If pluginA registers saved object type `plugin_a_type` + then pluginB must never register that same type, even if pluginA is + disabled. Although we cannot enforce it on third-party plugins, breaking + this assumption may lead to data loss. + +## 4.1 Discover and remedy potential failures before any downtime + +> Achieves goals: (2.3) +> Mitigates errors: (3.1), (3.2) + +1. Introduce a CLI option to perform a dry run migration to allow + administrators to locate and fix potential migration failures without + taking their existing Kibana node(s) offline. +2. To have the highest chance of surfacing potential failures such as low disk + space, dry run migrations should not be mere simulations. A dry run should + perform a real migration in a way that doesn’t impact the existing Kibana + cluster. +3. The CLI should generate a migration report to make it easy to create a + support request from a failed migration dry run. + 1. The report would be an NDJSON export of all failed objects. + 2. If support receives such a report, we could modify all the objects to + ensure the migration would pass and send this back to the client. + 3. The client can then import the updated objects using the standard Saved + Objects NDJSON import and run another dry run to verify all problems + have been fixed. +4. Make running dry run migrations a required step in the upgrade procedure + documentation. +5. (Optional) Add dry run migrations to the standard cloud upgrade procedure? + +## 4.2 Automatically retry failed migrations until they succeed + +> Achieves goals: (2.2), (2.6) +> Mitigates errors (3.3) and (3.4) + +External conditions such as failures from an unhealthy Elasticsearch cluster +(3.3) can cause the migration to fail. The Kibana cluster should be able to +recover automatically once these external conditions are resolved. There are +two broad approaches to solving this problem based on whether or not +migrations are idempotent: + +| Idempotent migrations |Description | +| --------------------- | --------------------------------------------------------- | +| Yes | Idempotent migrations performed without coordination | +| No | Single node migrations coordinated through a lease / lock | + +Idempotent migrations don't require coordination making the algorithm +significantly less complex and will never require manual intervention to +retry. We, therefore, prefer this solution, even though it introduces +restrictions on migrations (4.2.1.1). For other alternatives that were +considered see section [(5)](#5-alternatives). + +## 4.2.1 Idempotent migrations performed without coordination + +The migration system can be said to be idempotent if the same results are +produced whether the migration was run once or multiple times. This property +should hold even if new (up to date) writes occur in between migration runs +which introduces the following restrictions: + +### 4.2.1.1 Restrictions + +1. All document transforms need to be deterministic, that is a document + transform will always return the same result for the same set of inputs. +2. It should always be possible to construct the exact set of inputs required + for (1) at any point during the migration process (before, during, after). + +Although these restrictions require significant changes, it does not prevent +known upcoming migrations such as [sharing saved-objects in multiple spaces](https://github.com/elastic/kibana/issues/27004) or [splitting a saved +object into multiple child +documents](https://github.com/elastic/kibana/issues/26602). To ensure that +these migrations are idempotent, they will have to generate new saved object +id's deterministically with e.g. UUIDv5. + + +### 4.2.1.2 Migration algorithm: Cloned index per version +Note: +- The description below assumes the migration algorithm is released in 7.10.0. + So < 7.10.0 will use `.kibana` and >= 7.10.0 will use `.kibana_current`. +- We refer to the alias and index that outdated nodes use as the source alias + and source index. +- Every version performs a migration even if mappings or documents aren't outdated. + +1. Locate the source index by fetching aliases (including `.kibana` for + versions prior to v7.10.0) + + ``` + GET '/_alias/.kibana_current,.kibana_7.10.0,.kibana' + ``` + + The source index is: + 1. the index the `.kibana_current` alias points to, or if it doesn’t exist, + 2. the index the `.kibana` alias points to, or if it doesn't exist, + 3. the v6.x `.kibana` index + + If none of the aliases exists, this is a new Elasticsearch cluster and no + migrations are necessary. Create the `.kibana_7.10.0_001` index with the + following aliases: `.kibana_current` and `.kibana_7.10.0`. +2. If `.kibana_current` and `.kibana_7.10.0` both exists and are pointing to the same index this version's migration has already been completed. + 1. Because the same version can have plugins enabled at any point in time, + perform the mappings update in step (6) and migrate outdated documents + with step (7). + 2. Skip to step (9) to start serving traffic. +3. Fail the migration if: + 1. `.kibana_current` is pointing to an index that belongs to a later version of Kibana .e.g. `.kibana_7.12.0_001` + 2. (Only in 8.x) The source index contains documents that belong to an unknown Saved Object type (from a disabled plugin). Log an error explaining that the plugin that created these documents needs to be enabled again or that these objects should be deleted. See section (4.2.1.4). +4. Mark the source index as read-only and wait for all in-flight operations to drain (requires https://github.com/elastic/elasticsearch/pull/58094). This prevents any further writes from outdated nodes. Assuming this API is similar to the existing `//_close` API, we expect to receive `"acknowledged" : true` and `"shards_acknowledged" : true`. If all shards don’t acknowledge within the timeout, retry the operation until it succeeds. +5. Clone the source index into a new target index which has writes enabled. All nodes on the same version will use the same fixed index name e.g. `.kibana_7.10.0_001`. The `001` postfix isn't used by Kibana, but allows for re-indexing an index should this be required by an Elasticsearch upgrade. E.g. re-index `.kibana_7.10.0_001` into `.kibana_7.10.0_002` and point the `.kibana_7.10.0` alias to `.kibana_7.10.0_002`. + 1. `POST /.kibana_n/_clone/.kibana_7.10.0_001?wait_for_active_shards=all {"settings": {"index.blocks.write": false}}`. Ignore errors if the clone already exists. + 2. Wait for the cloning to complete `GET /_cluster/health/.kibana_7.10.0_001?wait_for_status=green&timeout=60s` If cloning doesn’t complete within the 60s timeout, log a warning for visibility and poll again. +6. Update the mappings of the target index + 1. Retrieve the existing mappings including the `migrationMappingPropertyHashes` metadata. + 2. Update the mappings with `PUT /.kibana_7.10.0_001/_mapping`. The API deeply merges any updates so this won't remove the mappings of any plugins that were enabled in a previous version but are now disabled. + 3. Ensure that fields are correctly indexed using the target index's latest mappings `POST /.kibana_7.10.0_001/_update_by_query?conflicts=proceed`. In the future we could optimize this query by only targeting documents: + 1. That belong to a known saved object type. + 2. Which don't have outdated migrationVersion numbers since these will be transformed anyway. + 3. That belong to a type whose mappings were changed by comparing the `migrationMappingPropertyHashes`. (Metadata, unlike the mappings isn't commutative, so there is a small chance that the metadata hashes do not accurately reflect the latest mappings, however, this will just result in an less efficient query). +7. Transform documents by reading batches of outdated documents from the target index then transforming and updating them with optimistic concurrency control. + 1. Ignore any version conflict errors. + 2. If a document transform throws an exception, add the document to a failure list and continue trying to transform all other documents. If any failures occured, log the complete list of documents that failed to transform. Fail the migration. +8. Mark the migration as complete by doing a single atomic operation (requires https://github.com/elastic/elasticsearch/pull/58100) that: + 1. Checks that `.kibana-current` alias is still pointing to the source index + 2. Points the `.kibana-7.10.0` and `.kibana_current` aliases to the target index. + 3. If this fails with a "required alias [.kibana_current] does not exist" error fetch `.kibana_current` again: + 1. If `.kibana_current` is _not_ pointing to our target index fail the migration. + 2. If `.kibana_current` is pointing to our target index the migration has succeeded and we can proceed to step (9). +9. Start serving traffic. + +Together with the limitations, this algorithm ensures that migrations are +idempotent. If two nodes are started simultaneously, both of them will start +transforming documents in that version's target index, but because migrations are idempotent, it doesn’t matter which node’s writes win. + +
+ In the future, this algorithm could enable (2.6) "read-only functionality during the downtime window" but this is outside of the scope of this RFC. + + Although the migration algorithm guarantees there's no data loss while providing read-only access to outdated nodes, this could cause plugins to behave in unexpected ways. If we wish to persue it in the future, enabling read-only functionality during the downtime window will be it's own project and must include an audit of all plugins' behaviours. +
+ +### 4.2.1.3 Upgrade and rollback procedure +When a newer Kibana starts an upgrade, it blocks all writes to the outdated index to prevent data loss. Since Kibana is not designed to gracefully handle a read-only index this could have unintended consequences such as a task executing multiple times but never being able to write that the task was completed successfully. To prevent unintended consequences, the following procedure should be followed when upgrading Kibana: + +1. Gracefully shutdown outdated nodes by sending a `SIGTERM` signal + 1. Node starts returning `503` from it's healthcheck endpoint to signal to + the load balancer that it's no longer accepting new traffic (requires https://github.com/elastic/kibana/issues/46984). + 2. Allows ungoing HTTP requests to complete with a configurable timeout + before forcefully terminating any open connections. + 3. Closes any keep-alive sockets by sending a `connection: close` header. + 4. Shutdown all plugins and Core services. +2. (recommended) Take a snapshot of all Kibana's Saved Objects indices. This simplifies doing a rollback to a simple snapshot restore, but is not required in order to do a rollback if a migration fails. +3. Start the upgraded Kibana nodes. All running Kibana nodes should be on the same version, have the same plugins enabled and use the same configuration. + +To rollback to a previous version of Kibana with a snapshot +1. Shutdown all Kibana nodes. +2. Restore the Saved Object indices and aliases from the snapshot +3. Start the rollback Kibana nodes. All running Kibana nodes should be on the same rollback version, have the same plugins enabled and use the same configuration. + +To rollback to a previous version of Kibana without a snapshot: +(Assumes the migration to 7.11.0 failed) +1. Shutdown all Kibana nodes. +2. Remove the index created by the failed Kibana migration by using the version-specific alias e.g. `DELETE /.kibana_7.11.0` +3. Identify the rollback index: + 1. If rolling back to a Kibana version < 7.10.0 use `.kibana` + 2. If rolling back to a Kibana version >= 7.10.0 use the version alias of the Kibana version you wish to rollback to e.g. `.kibana_7.10.0` +4. Point the `.kibana_current` alias to the rollback index. +5. Remove the write block from the rollback index. +6. Start the rollback Kibana nodes. All running Kibana nodes should be on the same rollback version, have the same plugins enabled and use the same configuration. + +### 4.2.1.4 Handling documents that belong to a disabled plugin +It is possible for a plugin to create documents in one version of Kibana, but then when upgrading Kibana to a newer version, that plugin is disabled. Because the plugin is disabled it cannot register it's Saved Objects type including the mappings or any migration transformation functions. These "orphan" documents could cause future problems: + - A major version introduces breaking mapping changes that cannot be applied to the data in these documents. + - Two majors later migrations will no longer be able to migrate this old schema and could fail unexpectadly when the plugin is suddenly enabled. + +As a concrete example of the above, consider a user taking the following steps: +1. Installs Kibana 7.6.0 with spaces=enabled. The spaces plugin creates a default space saved object. +2. User upgrades to 7.10.0 but uses the OSS download which has spaces=disabled. Although the 7.10.0 spaces plugin includes a migration for space documents, the OSS release cannot migrate the documents or update it's mappings. +3. User realizes they made a mistake and use Kibana 7.10.0 with x-pack and the spaces plugin enabled. At this point we have a completed migration for 7.10.0 but there's outdated spaces documents with migrationVersion=7.6.0 instead of 7.10.0. + +There are several approaches we could take to dealing with these orphan documents: + +1. Start up but refuse to query on types with outdated documents until a user manually triggers a re-migration + + Advantages: + - The impact is limited to a single plugin + + Disadvantages: + - It might be less obvious that a plugin is in a degraded state unless you read the logs (not possible on Cloud) or view the `/status` endpoint. + - If a user doesn't care that the plugin is degraded, orphan documents are carried forward indefinitely. + - Since Kibana has started receiving traffic, users can no longer + downgrade without losing data. They have to re-migrate, but if that + fails they're stuck. + - Introduces a breaking change in the upgrade behaviour + + To perform a re-migration: + - Remove the `.kibana_7.10.0` alias + - Take a snapshot OR set the configuration option `migrations.target_index_postfix: '002'` to create a new target index `.kibana_7.10.0_002` and keep the `.kibana_7.10.0_001` index to be able to perform a rollback. + - Start up Kibana + +2. Refuse to start Kibana until the plugin is enabled or it's data deleted + + Advantages: + - Admin’s are forced to deal with the problem as soon as they disable a plugin + + Disadvantages: + - Cannot temporarily disable a plugin to aid in debugging or to reduce the load a Kibana plugin places on an ES cluster. + - Introduces a breaking change + +3. Refuse to start a migration until the plugin is enabled or it's data deleted + + Advantages: + - We force users to enable a plugin or delete the documents which prevents these documents from creating future problems like a mapping update not being compatible because there are fields which are assumed to have been migrated. + - We keep the index “clean”. + + Disadvantages: + - Since users have to take down outdated nodes before they can start the upgrade, they have to enter the downtime window before they know about this problem. This prolongs the downtime window and in many cases might cause an operations team to have to reschedule their downtime window to give them time to investigate the documents that need to be deleted. Logging an error on every startup could warn users ahead of time to mitigate this. + - We don’t expose Kibana logs on Cloud so this will have to be escalated to support and could take 48hrs to resolve (users can safely rollback, but without visibility into the logs they might not know this). Exposing Kibana logs is on the cloud team’s roadmap. + - It might not be obvious just from the saved object type, which plugin created these objects. + - Introduces a breaking change in the upgrade behaviour + +4. Use a hash of enabled plugins as part of the target index name + Using a migration target index name like + `.kibana_7.10.0_${hash(enabled_plugins)}_001` we can migrate all documents + every time a plugin is enabled / disabled. + + Advantages: + - Outdated documents belonging to disabled plugins will be upgraded as soon + as the plugin is enabled again. + + Disadvantages: + - Disabling / enabling a plugin will cause downtime (breaking change). + - When a plugin is enabled, disabled and enabled again our target index + will be an existing outdated index which needs to be deleted and + re-cloned. Without a way to check if the index is outdated, we cannot + deterministically perform the delete and re-clone operation without + coordination. + +5. Transform outdated documents (step 7) on every startup + Advantages: + - Outdated documents belonging to disabled plugins will be upgraded as soon + as the plugin is enabled again. + + Disadvantages: + - Orphan documents are retained indefinitely so there's still a potential + for future problems. + - Slightly slower startup time since we have to query for outdated + documents every time. + +We prefer option (3) since it provides flexibility for disabling plugins in +the same version while also protecting users' data in all cases during an +upgrade migration. However, because this is a breaking change we will +implement (5) during 7.x and only implement (3) during 8.x. + +# 5. Alternatives +## 5.1 Rolling upgrades +We considered implementing rolling upgrades to provide zero downtime +migrations. However, this would introduce significant complexity for plugins: +they will need to maintain up and down migration transformations and ensure +that queries match both current and outdated documents across all +versions. Although we can afford the once-off complexity of implementing +rolling upgrades, the complexity burden of maintaining plugins that support +rolling-upgrades will slow down all development in Kibana. Since a predictable +downtime window is sufficient for our users, we decided against trying to +achieve zero downtime with rolling upgrades. See "Rolling upgrades" in +https://github.com/elastic/kibana/issues/52202 for more information. + +## 5.2 Single node migrations coordinated through a lease/lock +This alternative is a proposed algorithm for coordinating migrations so that +these only happen on a single node and therefore don't have the restrictions +found in [(4.2.1.1)](#4311-restrictions). We decided against this algorithm +primarily because it is a lot more complex, but also because it could still +require manual intervention to retry from certain unlikely edge cases. + +
+ It's impossible to guarantee that a single node performs the + migration and automatically retry failed migrations. + +Coordination should ensure that only one Kibana node performs the migration at +a given time which can be achived with a distributed lock built on top of +Elasticsearch. For the Kibana cluster to be able to retry a failed migration, +requires a specialized lock which expires after a given amount of inactivity. +We will refer to such expiring locks as a "lease". + +If a Kibana process stalls, it is possible that the process' lease has expired +but the process doesn't yet recognize this and continues the migration. To +prevent this from causing data loss each lease should be accompanied by a +"guard" that prevents all writes after the lease has expired. See +[how to do distributed +locking](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html) +for an in-depth discussion. + +Elasticsearch doesn't provide any building blocks for constructing such a guard. +
+ +However, we can implement a lock (that never expires) with strong +data-consistency guarantees. Because there’s no expiration, a failure between +obtaining the lock and releasing it will require manual intervention. Instead +of trying to accomplish the entire migration after obtaining a lock, we can +only perform the last step of the migration process, moving the aliases, with +a lock. A permanent failure in only this last step is not impossible, but very +unlikely. + +### 5.2.1 Migration algorithm +1. Obtain a document lock (see [5.2.2 Document lock + algorithm](#522-document-lock-algorithm)). Convert the lock into a "weak + lease" by expiring locks for nodes which aren't active (see [4.2.2.4 + Checking for lease expiry](#4324-checking-for-lease-expiry)). This "weak + lease" doesn't require strict guarantees since it's only used to prevent + multiple Kibana nodes from performing a migration in parallel to reduce the + load on Elasticsearch. +2. Migrate data into a new process specific index (we could use the process + UUID that’s used in the lease document like + `.kibana_3ef25ff1-090a-4335-83a0-307a47712b4e`). +3. Obtain a document lock (see [5.2.2 Document lock + algorithm](#522-document-lock-algorithm)). +4. Finish the migration by pointing `.kibana` → + `.kibana_3ef25ff1-090a-4335-83a0-307a47712b4e`. This automatically releases + the document lock (and any leases) because the new index will contain an + empty `kibana_cluster_state`. + +If a process crashes or is stopped after (3) but before (4) the lock will have +to be manually removed by deleting the `kibana_cluster_state` document from +`.kibana` or restoring from a snapshot. + +### 5.2.2 Document lock algorithm +To improve on the existing Saved Objects migrations lock, a locking algorithm +needs to satisfy the following requirements: +- Must guarantee that only a single node can obtain the lock. Since we can + only provide strong data-consistency guarantees on the document level in + Elasticsearch our locking mechanism needs to be based on a document. +- Manually removing the lock + - shouldn't have any risk of accidentally causing data loss. + - can be done with a single command that's always the same (shouldn’t + require trying to find `n` for removing the correct `.kibana_n` index). +- Must be easy to retrieve the lock/cluster state to aid in debugging or to + provide visibility. + +Algorithm: +1. Node reads `kibana_cluster_state` lease document from `.kibana` +2. It sends a heartbeat every `heartbeat_interval` seconds by sending an + update operation that adds it’s UUID to the `nodes` array and sets the + `lastSeen` value to the current local node time. If the update fails due to + a version conflict the update operation is retried after a random delay by + fetching the document again and attempting the update operation once more. +3. To obtain a lease, a node: + 1. Fetches the `kibana_cluster_state` document + 2. If all the nodes’ `hasLock === false` it sets it’s own `hasLock` to + true and attempts to write the document. If the update fails + (presumably because of another node’s heartbeat update) it restarts the + process to obtain a lease from step (3). + 3. If another nodes’ `hasLock === true` the node failed to acquire a + lock and waits until the active lock has expired before attempting to + obtain a lock again. +4. Once a node is done with its lock, it releases it by fetching and then + updating `hasLock = false`. The fetch + update operations are retried until + this node’s `hasLock === false`. + +Each machine writes a `UUID` to a file, so a single machine may have multiple +processes with the same Kibana `UUID`, so we should rather generate a new UUID +just for the lifetime of this process. + +`KibanaClusterState` document format: +```js + nodes: { + "852bd94e-5121-47f3-a321-e09d9db8d16e": { + version: "7.6.0", + lastSeen: [ 1114793, 555149266 ], // hrtime() big int timestamp + hasLease: true, + hasLock: false, + }, + "8d975c5b-cbf6-4418-9afb-7aa3ea34ac90": { + version: "7.6.0", + lastSeen: [ 1114862, 841295591 ], + hasLease: false, + hasLock: false, + }, + "3ef25ff1-090a-4335-83a0-307a47712b4e": { + version: "7.6.0", + lastSeen: [ 1114877, 611368546 ], + hasLease: false, + hasLock: false, + }, + }, + oplog: [ + {op: 'ACQUIRE_LOCK', node: '852bd94e...', timestamp: '2020-04-20T11:58:56.176Z'} + ] +} +``` + +### 5.2.3 Checking for "weak lease" expiry +The simplest way to check for lease expiry is to inspect the `lastSeen` value. +If `lastSeen + expiry_timeout > now` the lock is considered expired. If there +are clock drift or daylight savings time adjustments, there’s a risk that a +node loses it’s lease before `expiry_timeout` has occurred. Since losing a +lock prematurely will not lead to data loss it’s not critical that the +expiry time is observed under all conditions. + +A slightly safer approach is to use a monotonically increasing clock +(`process.hrtime()`) and relative time to determine expiry. Using a +monotonically increasing clock guarantees that the clock will always increase +even if the system time changes due to daylight savings time, NTP clock syncs, +or manually setting the time. To check for expiry, other nodes poll the +cluster state document. Once they see that the `lastSeen` value has increased, +they capture the current hr time `current_hr_time` and starts waiting until +`process.hrtime() - current_hr_time > expiry_timeout` if at that point +`lastSeen` hasn’t been updated the lease is considered to have expired. This +means other nodes can take up to `2*expiry_timeout` to recognize an expired +lease, but a lease will never expire prematurely. + +Any node that detects an expired lease can release that lease by setting the +expired node’s `hasLease = false`. It can then attempt to acquire its lease. + +## 5.3 Minimize data loss with mixed Kibana versions during 7.x +When multiple versions of Kibana are running at the same time, writes from the +outdated node can end up either in the outdated Kibana index, the newly +migrated index, or both. New documents added (and some updates) into the old +index while a migration is in-progress will be lost. Writes that end up in the +new index will be in an outdated format. This could cause queries on the data +to only return a subset of the results which leads to incorrect results or +silent data loss. + +Minimizing data loss from mixed 7.x versions, introduces two additional steps +to rollback to a previous version without a snapshot: +1. (existing) Point the `.kibana` alias to the previous Kibana index `.kibana_n-1` +2. (existing) Delete `.kibana_n` +3. (new) Enable writes on `.kibana_n-1` +4. (new) Delete the dummy "version lock" document from `.kibana_n-1` + +Since our documentation and server logs have implicitly encouraged users to +rollback without using snapshots, many users might have to rely on these +additional migration steps to perform a rollback. Since even the existing +steps are error prone, introducing more steps will likely introduce more +problems than what it solves. + +1. All future versions of Kibana 7.x will use the `.kibana_saved_objects` + alias to locate the current index. If `.kibana_saved_objects` doesn't + exist, newer versions will fallback to reading `.kibana`. +2. All future versions of Kibana will locate the index that + `.kibana_saved_objects` points to and then read and write directly from + the _index_ instead of the alias. +3. Before starting a migration: + 1. Write a new dummy "version lock" document to the `.kibana` index with a + `migrationVersion` set to the current version of Kibana. If an outdated + node is started up after a migration was started it will detect that + newer documents are present in the index and refuse to start up. + 2. Set the outdated index to read-only. Since `.kibana` is never advanced, + it will be pointing to a read-only index which prevent writes from + 6.8+ releases which are already online. + +## 5.4 In-place migrations that re-use the same index (8.0) +> We considered an algorithm that re-uses the same index for migrations and an approach to minimize data-loss if our upgrade procedures aren't followed. This is no longer our preferred approach because of several downsides: +> - It requires taking snapshots to prevent data loss so we can only release this in 8.x +> - Minimizing data loss with unsupported upgrade configurations adds significant complexity and still doesn't guarantee that data isn't lost. + +### 5.4.1 Migration algorithm (8.0): +1. Exit Kibana with a fatal error if a newer node has started a migration by + checking for: + 1. Documents with a newer `migrationVersion` numbers. +2. If the mappings are out of date, update the mappings to the combination of + the index's current mappings and the expected mappings. +3. If there are outdated documents, migrate these in batches: + 1. Read a batch of outdated documents from the index. + 2. Transform documents by applying the migration transformation functions. + 3. Update the document batch in the same index using optimistic concurrency + control. If a batch fails due to an update version mismatch continue + migrating the other batches. + 4. If a batch fails due other reasons repeat the entire migration process. +4. If any of the batches in step (3.3) failed, repeat the entire migration + process. This ensures that in-progress bulk update operations from an + outdated node won't lead to unmigrated documents still being present after + the migration. +5. Once all documents are up to date, the migration is complete and Kibana can + start serving traffic. + +Advantages: +- Not duplicating all documents into a new index will speed up migrations and + reduce the downtime window. This will be especially important for the future + requirement to support > 10k or > 100k documents. +- We can check the health of an existing index before starting the migration, + but we cannot detect what kind of failures might occur while creating a new + index. Whereas retrying migrations will eventually recover from the errors + in (3.3), re-using an index allows us to detect these problems before trying + and avoid errors like (3.3.1) altogether. +- Single index to backup instead of “index pattern” that matches any + `.kibana_n`. +- Simplifies Kibana system index Elasticsearch plugin since it needs to work + on one index per "tenant". +- By leveraging optimistic concurrency control we can further minimize data + loss for unsupported upgrade configurations in the future. + +Drawbacks: +- Cannot make breaking mapping changes (even though it was possible, we have not + introduced a breaking mapping change during 7.x). +- Rollback is only possible by restoring a snapshot which requires educating + users to ensure that they don't rely on `.kibana_n` indices as backups. + (Apart from the need to educate users, snapshot restores provide many + benefits). +- It narrows the second restriction under (4.2.1) even further: migrations + cannot rely on any state that could change as part of a migration because we + can no longer use the previous index as a snapshot of unmigrated state. +- We can’t automatically perform a rollback from a half-way done migration. +- It’s impossible to provide read-only functionality for outdated nodes which + means we can't achieve goal (2.7). + +### 5.4.2 Minimizing data loss with unsupported upgrade configurations (8.0) +> This alternative can reduce some data loss when our upgrade procedure isn't +> followed with the algorithm in (5.4.1). + +Even if (4.5.2) is the only supported upgrade procedure, we should try to +prevent data loss when these instructions aren't followed. + +To prevent data loss we need to prevent any writes from older nodes. We use +a version-specific alias for this purpose. Each time a migration is started, +all other aliases are removed. However, aliases are stored inside +Elasticsearch's ClusterState and this state could remain inconsistent between +nodes for an unbounded amount of time. In addition, bulk operations that were +accepted before the alias was removed will continue to run even after removing +the alias. + +As a result, Kibana cannot guarantee that there would be no data loss but +instead, aims to minimize it as much as possible by adding the bold sections +to the migration algorithm from (5.4.1) + +1. **Disable `action.auto_create_index` for the Kibana system indices.** +2. Exit Kibana with a fatal error if a newer node has started a migration by + checking for: + 1. **Version-specific aliases on the `.kibana` index with a newer version.** + 2. Documents with newer `migrationVersion` numbers. +3. **Remove all other aliases and create a new version-specific alias for + reading and writing to the `.kibana` index .e.g `.kibana_8.0.1`. During and + after the migration, all saved object reads and writes use this alias + instead of reading or writing directly to the index. By using the atomic + `POST /_aliases` API we minimize the chance that an outdated node creating + new outdated documents can cause data loss.** +4. **Wait for the default bulk operation timeout of 30s. This ensures that any + bulk operations accepted before the removal of the alias have either + completed or returned a timeout error to it's initiator.** +5. If the mappings are out of date, update the mappings **through the alias** + to the combination of the index's current mappings and the expected + mappings. **If this operation fails due to an index missing exception (most + likely because another node removed our version-specific alias) repeat the + entire migration process.** +6. If there are outdated documents, migrate these in batches: + 1. Read a batch of outdated documents from `.kibana_n`. + 2. Transform documents by applying the migration functions. + 3. Update the document batch in the same index using optimistic concurrency + control. If a batch fails due to an update version mismatch continue + migrating the other batches. + 4. If a batch fails due other reasons repeat the entire migration process. +7. If any of the batches in step (6.3) failed, repeat the entire migration + process. This ensures that in-progress bulk update operations from an + outdated node won't lead to unmigrated documents still being present after + the migration. +8. Once all documents are up to date, the migration is complete and Kibana can + start serving traffic. + +Steps (2) and (3) from the migration algorithm in minimize the chances of the +following scenarios occuring but cannot guarantee it. It is therefore useful +to enumarate some scenarios and their worst case impact: +1. An outdated node issued a bulk create to it's version-specific alias. + Because a user doesn't wait for all traffic to drain a newer node starts + it's migration before the bulk create was complete. Since this bulk create + was accepted before the newer node deleted the previous version-specific + aliases, it is possible that the index now contains some outdated documents + that the new node is unaware of and doesn't migrate. Although these outdated + documents can lead to inconsistent query results and data loss, step (4) + ensures that an error will be returned to the node that created these + objects. +2. A 8.1.0 node and a 8.2.0 node starts migrating a 8.0.0 index in parallel. + Even though the 8.2.0 node will remove the 8.1.0 version-specific aliases, + the 8.1.0 node could have sent an bulk update operation that got accepted + before its alias was removed. When the 8.2.0 node tries to migrate these + 8.1.0 documents it gets a version conflict but cannot be sure if this was + because another node of the same version migrated this document (which can + safely be ignored) or interference from a different Kibana version. The + 8.1.0 node will hit the error in step (6.3) and restart the migration but + then ultimately fail at step (2). The 8.2.0 node will repeat the entire + migration process from step (7) thus ensuring that all documents are up to + date. +3. A race condition with another Kibana node on the same version, but with + different enabled plugins caused this node's required mappings to be + overwritten. If this causes a mapper parsing exception in step (6.3) we can + restart the migration. Because updating the mappings is additive and saved + object types are unique to a plugin, restarting the migration will allow + the node to update the mappings to be compatible with node's plugins. Both + nodes will be able to successfully complete the migration of their plugins' + registered saved object types. However, if the migration doesn't trigger a + mapper parsing exception the incompatible mappings would go undetected + which can cause future problems like write failures or inconsistent query + results. + +## 5.5 Tag objects as “invalid” if their transformation fails +> This alternative prevents a failed migration when there's a migration transform function bug or a document with invalid data. Although it seems preferable to not fail the entire migration because of a single saved object type's migration transform bug or a single invalid document this has several pitfalls: +> 1. When an object fails to migrate the data for that saved object type becomes inconsistent. This could load to a critical feature being unavailable to a user leaving them with no choice but to downgrade. +> 2. Because Kibana starts accepting traffic after encountering invalid objects a rollback will lead to data loss leaving users with no clean way to recover. +> As a result we prefer to let an upgrade fail and making it easy for users to rollback until they can resolve the root cause. + +> Achieves goals: (2.2) +> Mitigates Errors (3.1), (3.2) + +1. Tag objects as “invalid” if they cause an exception when being transformed, + but don’t fail the entire migration. +2. Log an error message informing administrators that there are invalid + objects which require inspection. For each invalid object, provide an error + stack trace to aid in debugging. +3. Administrators should be able to generate a migration report (similar to + the one dry run migrations create) which is an NDJSON export of all objects + tagged as “invalid”. + 1. Expose this as an HTTP API first + 2. (later) Notify administrators and allow them to export invalid objects + from the Kibana UI. +4. When an invalid object is read, the Saved Objects repository will throw an + invalid object exception which should include a link to the documentation + to help administrators resolve migration bugs. +5. Educate Kibana developers to no longer simply write back an unmigrated + document if an exception occurred. A migration function should either + successfully transform the object or throw. + +# 6. How we teach this +1. Update documentation and server logs to start educating users to depend on + snapshots for Kibana rollbacks. +2. Update developer documentation and educate developers with best practices + for writing migration functions. + +# 7. Unresolved questions +1. When cloning an index we can only ever add new fields to the mappings. When + a saved object type or specific field is removed, the mappings will remain + until we re-index. Is it sufficient to only re-index every major? How do we + track the field count as it grows over every upgrade? +2. More generally, how do we deal with the growing field count approaching the + default limit of 1000? diff --git a/scripts/es.js b/scripts/es.js index 93f1d69350bac..2d56496f2fdd2 100644 --- a/scripts/es.js +++ b/scripts/es.js @@ -17,7 +17,7 @@ * under the License. */ -require('../src/setup_node_env'); +require('../src/setup_node_env/prebuilt_dev_only_entry'); var resolve = require('path').resolve; var pkg = require('../package.json'); diff --git a/scripts/load_team_assignment.js b/scripts/generate_team_assignments.js similarity index 96% rename from scripts/load_team_assignment.js rename to scripts/generate_team_assignments.js index b8f5edc833634..9dcb9bb90e0fd 100644 --- a/scripts/load_team_assignment.js +++ b/scripts/generate_team_assignments.js @@ -18,4 +18,4 @@ */ require('../src/setup_node_env'); -require('../src/dev/code_coverage/ingest_coverage/team_assignment').uploadTeamAssignmentJson(); +require('../src/dev/code_coverage/ingest_coverage/team_assignment').generateTeamAssignments(); diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx index e3f992990f9f9..d982136422268 100644 --- a/src/core/public/application/integration_tests/router.test.tsx +++ b/src/core/public/application/integration_tests/router.test.tsx @@ -107,7 +107,7 @@ describe('AppRouter', () => { expect(app1.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app1 html: App 1
" @@ -119,7 +119,7 @@ describe('AppRouter', () => { expect(app1Unmount).toHaveBeenCalled(); expect(app2.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app2 html:
App 2
" @@ -133,7 +133,7 @@ describe('AppRouter', () => { expect(standardApp.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app1 html: App 1
" @@ -145,7 +145,7 @@ describe('AppRouter', () => { expect(standardAppUnmount).toHaveBeenCalled(); expect(chromelessApp.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-a/path html:
Chromeless A
" @@ -157,7 +157,7 @@ describe('AppRouter', () => { expect(chromelessAppUnmount).toHaveBeenCalled(); expect(standardApp.mounter.mount).toHaveBeenCalledTimes(2); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app1 html: App 1
" @@ -171,7 +171,7 @@ describe('AppRouter', () => { expect(chromelessAppA.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-a/path html:
Chromeless A
" @@ -183,7 +183,7 @@ describe('AppRouter', () => { expect(chromelessAppAUnmount).toHaveBeenCalled(); expect(chromelessAppB.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-b/path html:
Chromeless B
" @@ -195,7 +195,7 @@ describe('AppRouter', () => { expect(chromelessAppBUnmount).toHaveBeenCalled(); expect(chromelessAppA.mounter.mount).toHaveBeenCalledTimes(2); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-a/path html:
Chromeless A
" diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx index 089d1cf3f3ced..9821db5ba2666 100644 --- a/src/core/public/application/ui/app_container.tsx +++ b/src/core/public/application/ui/app_container.tsx @@ -25,7 +25,7 @@ import React, { useState, MutableRefObject, } from 'react'; -import { EuiLoadingSpinner } from '@elastic/eui'; +import { EuiLoadingElastic } from '@elastic/eui'; import type { MountPoint } from '../../types'; import { AppLeaveHandler, AppStatus, AppUnmount, Mounter } from '../types'; @@ -120,7 +120,7 @@ export const AppContainer: FunctionComponent = ({ {appNotFound && } {showSpinner && (
- +
)}
diff --git a/src/core/public/chrome/ui/__snapshots__/loading_indicator.test.tsx.snap b/src/core/public/chrome/ui/__snapshots__/loading_indicator.test.tsx.snap index 3007be1e5dfe0..e6bf7e898d8c4 100644 --- a/src/core/public/chrome/ui/__snapshots__/loading_indicator.test.tsx.snap +++ b/src/core/public/chrome/ui/__snapshots__/loading_indicator.test.tsx.snap @@ -1,23 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`kbnLoadingIndicator is hidden by default 1`] = ` -
-
-
+/> `; exports[`kbnLoadingIndicator is visible when loadingCount is > 0 1`] = ` -
-
-
+/> `; diff --git a/src/core/public/chrome/ui/_loading_indicator.scss b/src/core/public/chrome/ui/_loading_indicator.scss index ad934717b4b76..ccf1ecc873fc5 100644 --- a/src/core/public/chrome/ui/_loading_indicator.scss +++ b/src/core/public/chrome/ui/_loading_indicator.scss @@ -1,55 +1,4 @@ -$kbnLoadingIndicatorBackgroundSize: $euiSizeXXL * 10; -$kbnLoadingIndicatorColor1: tint($euiColorAccent, 15%); -$kbnLoadingIndicatorColor2: tint($euiColorAccent, 60%); - -/** - * 1. Position this loader on top of the content. - * 2. Make sure indicator isn't wider than the screen. - */ -.kbnLoadingIndicator { - position: fixed; // 1 - top: 0; // 1 - left: 0; // 1 - right: 0; // 1 - z-index: $euiZLevel2; // 1 - overflow: hidden; // 2 - height: $euiSizeXS / 2; - - &.hidden { - visibility: hidden; - opacity: 0; - transition-delay: 0.25s; - } -} - -.kbnLoadingIndicator__bar { - top: 0; - left: 0; - right: 0; - bottom: 0; - position: absolute; - z-index: $euiZLevel2 + 1; - visibility: visible; - display: block; - animation: kbn-animate-loading-indicator 2s linear infinite; - background-color: $kbnLoadingIndicatorColor2; - background-image: linear-gradient( - to right, - $kbnLoadingIndicatorColor1 0%, - $kbnLoadingIndicatorColor1 50%, - $kbnLoadingIndicatorColor2 50%, - $kbnLoadingIndicatorColor2 100% - ); - background-repeat: repeat-x; - background-size: $kbnLoadingIndicatorBackgroundSize $kbnLoadingIndicatorBackgroundSize; - width: 200%; -} - -@keyframes kbn-animate-loading-indicator { - from { - transform: translateX(0); - } - to { - transform: translateX(-$kbnLoadingIndicatorBackgroundSize); - } +.kbnLoadingIndicator-hidden { + visibility: hidden; + animation-play-state: paused; } diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 1a4d93aee9516..8937ecb4d9b4e 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -1690,66 +1690,6 @@ exports[`Header renders 1`] = ` } } > - -
-
-
-
, + , ], }, Object { @@ -2971,6 +2963,81 @@ exports[`Header renders 1`] = `
+ +
+ + + + + +
+
; + return ; } const toggleCollapsibleNavRef = createRef(); @@ -97,7 +97,6 @@ export function Header({ return ( <> -
, + , ], borders: 'none', }, diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx index 174c46981db53..52412f8990c7a 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx @@ -20,7 +20,7 @@ import { EuiHeaderBreadcrumbs } from '@elastic/eui'; import classNames from 'classnames'; import React from 'react'; -import { useObservable } from 'react-use'; +import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; import { ChromeBreadcrumb } from '../../chrome_service'; diff --git a/src/core/public/chrome/ui/header/header_logo.tsx b/src/core/public/chrome/ui/header/header_logo.tsx index dee93ecb1a804..83e0c52ab3f3a 100644 --- a/src/core/public/chrome/ui/header/header_logo.tsx +++ b/src/core/public/chrome/ui/header/header_logo.tsx @@ -20,7 +20,7 @@ import { EuiHeaderLogo } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { useObservable } from 'react-use'; +import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; import Url from 'url'; import { ChromeNavLink } from '../..'; diff --git a/src/core/public/chrome/ui/header/header_nav_controls.tsx b/src/core/public/chrome/ui/header/header_nav_controls.tsx index 8d9d8097fd8e3..69b0e3bd8afe3 100644 --- a/src/core/public/chrome/ui/header/header_nav_controls.tsx +++ b/src/core/public/chrome/ui/header/header_nav_controls.tsx @@ -19,7 +19,7 @@ import { EuiHeaderSectionItem } from '@elastic/eui'; import React from 'react'; -import { useObservable } from 'react-use'; +import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; import { ChromeNavControl } from '../../nav_controls'; import { HeaderExtension } from './header_extension'; diff --git a/src/core/public/chrome/ui/loading_indicator.tsx b/src/core/public/chrome/ui/loading_indicator.tsx index 0209612eae08c..ca3e95f722ec5 100644 --- a/src/core/public/chrome/ui/loading_indicator.tsx +++ b/src/core/public/chrome/ui/loading_indicator.tsx @@ -17,6 +17,8 @@ * under the License. */ +import { EuiLoadingSpinner, EuiProgress } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import React from 'react'; import classNames from 'classnames'; import { Subscription } from 'rxjs'; @@ -25,9 +27,12 @@ import { HttpStart } from '../../http'; export interface LoadingIndicatorProps { loadingCount$: ReturnType; + showAsBar?: boolean; } export class LoadingIndicator extends React.Component { + public static defaultProps = { showAsBar: false }; + private loadingCountSubscription?: Subscription; state = { @@ -50,16 +55,35 @@ export class LoadingIndicator extends React.Component -
-
+ const ariaHidden = this.state.visible ? false : true; + + const ariaLabel = i18n.translate('core.ui.loadingIndicatorAriaLabel', { + defaultMessage: 'Loading content', + }); + + return !this.props.showAsBar ? ( + + ) : ( + ); } } diff --git a/src/core/public/overlays/banners/user_banner_service.tsx b/src/core/public/overlays/banners/user_banner_service.tsx index 643d95a1e3bb4..2b93c3e4b6c21 100644 --- a/src/core/public/overlays/banners/user_banner_service.tsx +++ b/src/core/public/overlays/banners/user_banner_service.tsx @@ -19,12 +19,11 @@ import React, { Fragment } from 'react'; import ReactDOM from 'react-dom'; -import ReactMarkdown from 'react-markdown'; import { filter } from 'rxjs/operators'; import { Subscription } from 'rxjs'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiCallOut, EuiButton } from '@elastic/eui'; +import { EuiCallOut, EuiButton, EuiLoadingSpinner } from '@elastic/eui'; import { I18nStart } from '../../i18n'; import { IUiSettingsClient } from '../../ui_settings'; @@ -36,6 +35,8 @@ interface StartDeps { uiSettings: IUiSettingsClient; } +const ReactMarkdownLazy = React.lazy(() => import('react-markdown')); + /** * Sets up the custom banner that can be specified in advanced settings. * @internal @@ -75,7 +76,15 @@ export class UserBannerService { } iconType="help" > - {content.trim()} + + +
+ } + > + + banners.remove(id!)}> { expect(sut.inspect()).to.be('Right(undefined)'); }); }); - describe(`'fromNullable`, () => { + describe(`fromNullable`, () => { it(`should continue processing if a truthy is calculated`, () => { attempt({ detail: 'x' }).fold( () => {}, @@ -64,4 +64,18 @@ describe(`either datatype functions`, () => { attempt(false).fold(expectNull, () => {}); }); }); + describe(`predicate fns`, () => { + it(`right.isRight() is true`, () => { + expect(Either.right('a').isRight()).to.be(true); + }); + it(`right.isLeft() is false`, () => { + expect(Either.right('a').isLeft()).to.be(false); + }); + it(`left.isLeft() is true`, () => { + expect(Either.left().isLeft()).to.be(true); + }); + it(`left.isRight() is true`, () => { + expect(Either.left().isRight()).to.be(false); + }); + }); }); diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js new file mode 100644 index 0000000000000..371695337ed56 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { enumeratePatterns } from '../team_assignment/enumerate_patterns'; +import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; + +const log = new ToolingLog({ + level: 'info', + writeTo: process.stdout, +}); + +describe(`enumeratePatterns`, () => { + it(`should resolve x-pack/plugins/reporting/server/browsers/extract/unzip.js to kibana-reporting`, () => { + const actual = enumeratePatterns(REPO_ROOT)(log)( + new Map([['x-pack/plugins/reporting', ['kibana-reporting']]]) + ); + + expect( + actual[0].includes( + 'x-pack/plugins/reporting/server/browsers/extract/unzip.js kibana-reporting' + ) + ).to.be(true); + }); + it(`should resolve src/plugins/charts/public/static/color_maps/color_maps.ts to kibana-app`, () => { + const actual = enumeratePatterns(REPO_ROOT)(log)( + new Map([['src/plugins/charts/public/static/color_maps', ['kibana-app']]]) + ); + + expect(actual[0][0]).to.be( + 'src/plugins/charts/public/static/color_maps/color_maps.ts kibana-app' + ); + }); + it(`should resolve x-pack/plugins/security_solution/public/common/components/exceptions/builder/translations.ts to kibana-security`, () => { + const short = 'x-pack/plugins/security_solution'; + const actual = enumeratePatterns(REPO_ROOT)(log)(new Map([[short, ['kibana-security']]])); + + expect( + actual[0].includes( + `${short}/public/common/components/exceptions/builder/translations.ts kibana-security` + ) + ).to.be(true); + }); +}); diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/enumeration_helpers.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/enumeration_helpers.test.js new file mode 100644 index 0000000000000..f480135b45ac6 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/__tests__/enumeration_helpers.test.js @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { tryPath } from '../team_assignment/enumeration_helpers'; + +describe(`enumeration helper fns`, () => { + describe(`tryPath`, () => { + describe(`w/o glob file paths`, () => { + it(`should return a right on an existing path`, () => { + const aPath = 'src/dev/code_coverage/ingest_coverage/ingest.js'; + const actual = tryPath(aPath); + expect(actual.isRight()).to.be(true); + }); + it(`should return a left on a non existing path`, () => { + const aPath = 'src/dev/code_coverage/ingest_coverage/does_not_exist.js'; + const actual = tryPath(aPath); + expect(actual.isLeft()).to.be(true); + }); + }); + describe(`with glob file paths`, () => { + it(`should not error when the glob expands to nothing, but instead return a Left`, () => { + const aPath = 'src/legacy/core_plugins/kibana/public/home/*.ts'; + const actual = tryPath(aPath); + expect(actual.isLeft()).to.be(true); + }); + it(`should return a right on a glob that does indeed expand`, () => { + const aPath = 'src/dev/code_coverage/ingest_coverage/*.js'; + const actual = tryPath(aPath); + expect(actual.isRight()).to.be(true); + }); + }); + }); +}); diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/ingest_helpers.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/ingest_helpers.test.js index 7ca7279e0d64c..f668c1f86f5b0 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/ingest_helpers.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/ingest_helpers.test.js @@ -18,13 +18,8 @@ */ import expect from '@kbn/expect'; -import { maybeTeamAssign, whichIndex } from '../ingest_helpers'; -import { - TOTALS_INDEX, - RESEARCH_TOTALS_INDEX, - RESEARCH_COVERAGE_INDEX, - // COVERAGE_INDEX, -} from '../constants'; +import { whichIndex } from '../ingest_helpers'; +import { TOTALS_INDEX, RESEARCH_TOTALS_INDEX, RESEARCH_COVERAGE_INDEX } from '../constants'; describe(`Ingest Helper fns`, () => { describe(`whichIndex`, () => { @@ -56,20 +51,4 @@ describe(`Ingest Helper fns`, () => { }); }); }); - describe(`maybeTeamAssign`, () => { - describe(`against a coverage index`, () => { - it(`should have the pipeline prop`, () => { - const actual = maybeTeamAssign(true, { a: 'blah' }); - expect(actual).to.have.property('pipeline'); - }); - }); - describe(`against a totals index`, () => { - describe(`for "prod"`, () => { - it(`should not have the pipeline prop`, () => { - const actual = maybeTeamAssign(false, { b: 'blah' }); - expect(actual).not.to.have.property('pipeline'); - }); - }); - }); - }); }); diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.json b/src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.json deleted file mode 100644 index 355c484a84fa3..0000000000000 --- a/src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "abc": "123" -} diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.txt b/src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.txt new file mode 100644 index 0000000000000..d8924bd563f30 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.txt @@ -0,0 +1,194 @@ +x-pack/plugins/dashboard_enhanced/public/index.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/mocks.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/plugin.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/index.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/i18n.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/index.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.test.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/index.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/collect_config_container.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.story.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.test.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/i18n.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/index.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/i18n.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/index.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/constants.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/i18n.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/index.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/index.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/index.ts kibana-app +x-pack/plugins/dashboard_enhanced/scripts/storybook.js kibana-app +x-pack/plugins/discover_enhanced/common/config.ts kibana-app +x-pack/plugins/discover_enhanced/common/index.ts kibana-app +x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts kibana-app +x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts kibana-app +x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts kibana-app +x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts kibana-app +x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts kibana-app +x-pack/plugins/discover_enhanced/public/actions/explore_data/index.ts kibana-app +x-pack/plugins/discover_enhanced/public/actions/explore_data/kibana_url.ts kibana-app +x-pack/plugins/discover_enhanced/public/actions/explore_data/shared.ts kibana-app +x-pack/plugins/discover_enhanced/public/actions/index.ts kibana-app +x-pack/plugins/discover_enhanced/public/index.ts kibana-app +x-pack/plugins/discover_enhanced/public/plugin.ts kibana-app +x-pack/plugins/discover_enhanced/server/config.ts kibana-app +x-pack/plugins/discover_enhanced/server/index.ts kibana-app +x-pack/plugins/discover_enhanced/server/plugin.ts kibana-app +x-pack/plugins/lens/common/api.ts kibana-app +x-pack/plugins/lens/common/constants.ts kibana-app +x-pack/plugins/lens/common/index.ts kibana-app +x-pack/plugins/lens/common/types.ts kibana-app +x-pack/plugins/lens/config.ts kibana-app +x-pack/plugins/lens/public/app_plugin/app.test.tsx kibana-app +x-pack/plugins/lens/public/app_plugin/app.tsx kibana-app +x-pack/plugins/lens/public/app_plugin/index.ts kibana-app +x-pack/plugins/lens/public/app_plugin/mounter.tsx kibana-app +x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx kibana-app +x-pack/plugins/lens/public/datatable_visualization/expression.tsx kibana-app +x-pack/plugins/lens/public/datatable_visualization/index.ts kibana-app +x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx kibana-app +x-pack/plugins/lens/public/datatable_visualization/visualization.tsx kibana-app +x-pack/plugins/lens/public/debounced_component/debounced_component.test.tsx kibana-app +x-pack/plugins/lens/public/debounced_component/debounced_component.tsx kibana-app +x-pack/plugins/lens/public/debounced_component/index.ts kibana-app +x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx kibana-app +x-pack/plugins/lens/public/drag_drop/drag_drop.tsx kibana-app +x-pack/plugins/lens/public/drag_drop/index.ts kibana-app +x-pack/plugins/lens/public/drag_drop/providers.test.tsx kibana-app +x-pack/plugins/lens/public/drag_drop/providers.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/expression_helpers.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/suggestion_helpers.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/index.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.test.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/index.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/format_column.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/index.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/merge_tables.test.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/merge_tables.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/mocks.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/service.test.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/service.tsx kibana-app +x-pack/plugins/lens/public/help_menu_util.tsx kibana-app +x-pack/plugins/lens/public/id_generator/id_generator.test.ts kibana-app +x-pack/plugins/lens/public/id_generator/id_generator.ts kibana-app +x-pack/plugins/lens/public/id_generator/index.ts kibana-app +x-pack/plugins/lens/public/index.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/state_helpers.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/index.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/document_field.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/index.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx kibana-app +x-pack/plugins/reporting/server/browsers/download/clean.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/download/download.test.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/download/download.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/download/index.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/download/util.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/extract/extract.js kibana-reporting +x-pack/plugins/reporting/server/browsers/extract/extract_error.js kibana-reporting +x-pack/plugins/reporting/server/browsers/extract/index.js kibana-reporting +x-pack/plugins/reporting/server/browsers/extract/unzip.js kibana-reporting +x-pack/plugins/reporting/server/browsers/index.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/install.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/network_policy.test.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/network_policy.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/safe_child_process.ts kibana-reporting +x-pack/plugins/reporting/server/config/config.ts kibana-reporting +x-pack/plugins/reporting/server/config/create_config.test.ts kibana-reporting +x-pack/plugins/reporting/server/config/create_config.ts kibana-reporting +x-pack/plugins/reporting/server/config/default_chromium_sandbox_disabled.test.ts kibana-reporting +x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/loader.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.test.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.test.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/types.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/utils.ts kibana-app +x-pack/plugins/lens/public/lens_ui_telemetry/factory.test.ts kibana-app diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/team_assignment.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/team_assignment.test.js deleted file mode 100644 index e597ffb5d2f4b..0000000000000 --- a/src/dev/code_coverage/ingest_coverage/__tests__/team_assignment.test.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import { fetch } from '../team_assignment/get_data'; -import { noop } from '../utils'; - -describe(`Team Assignment`, () => { - const mockPath = 'src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.json'; - describe(`fetch fn`, () => { - it(`should be a fn`, () => { - expect(typeof fetch).to.be('function'); - }); - describe(`applied to a path that exists`, () => { - it(`should return the contents of the path`, () => { - const sut = fetch(mockPath); - expect(sut.chain(JSON.parse)).to.have.property('abc'); - }); - }); - describe(`applied to an non-existing path`, () => { - it(`should return a Left with the error message within`, () => { - const expectLeft = (err) => - expect(err.message).to.contain('ENOENT: no such file or directory'); - - fetch('fake_path.json').fold(expectLeft, noop); - }); - }); - }); -}); diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js index 746bccc3d718a..b6d17f83e327e 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js @@ -18,9 +18,17 @@ */ import expect from '@kbn/expect'; -import { ciRunUrl, coveredFilePath, itemizeVcs, prokPrevious } from '../transforms'; +import { + ciRunUrl, + coveredFilePath, + itemizeVcs, + prokPrevious, + teamAssignment, + last, +} from '../transforms'; +import { ToolingLog } from '@kbn/dev-utils'; -describe(`Transform fn`, () => { +describe(`Transform fns`, () => { describe(`ciRunUrl`, () => { it(`should add the url when present in the environment`, () => { process.env.CI_RUN_URL = 'blah'; @@ -83,4 +91,59 @@ describe(`Transform fn`, () => { ); }); }); + describe(`teamAssignment`, () => { + const teamAssignmentsPathMOCK = + 'src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.txt'; + const coveredFilePath = 'x-pack/plugins/reporting/server/browsers/extract/unzip.js'; + const obj = { coveredFilePath }; + const log = new ToolingLog({ + level: 'info', + writeTo: process.stdout, + }); + + describe(`with a coveredFilePath of ${coveredFilePath}`, () => { + const expected = 'kibana-reporting'; + it(`should resolve to ${expected}`, async () => { + const actual = await teamAssignment(teamAssignmentsPathMOCK)(log)(obj); + const { team } = actual; + expect(team).to.eql(expected); + }); + }); + + describe(`with a coveredFilePath of src/plugins/charts/public/static/color_maps/color_maps.ts`, () => { + const expected = 'kibana-reporting'; + it(`should resolve to ${expected}`, async () => { + const actual = await teamAssignment(teamAssignmentsPathMOCK)(log)(obj); + const { team } = actual; + expect(team).to.eql(expected); + }); + }); + + describe(`last fn`, () => { + describe(`applied to n results`, () => { + it(`should pick the last one`, () => { + const nteams = `src/plugins/charts/public/static/color_maps/color_maps.ts kibana-app +src/plugins/charts/public/static/color_maps/color_maps.ts kibana-app-arch`; + + const actual = last(nteams); + + expect(actual).to.be( + 'src/plugins/charts/public/static/color_maps/color_maps.ts kibana-app-arch' + ); + }); + }); + describe(`applied to 1 result`, () => { + it(`should pick that 1 result`, () => { + const nteams = + 'src/plugins/charts/public/static/color_maps/color_maps.ts kibana-app-arch'; + + const actual = last(nteams); + + expect(actual).to.be( + 'src/plugins/charts/public/static/color_maps/color_maps.ts kibana-app-arch' + ); + }); + }); + }); + }); }); diff --git a/src/dev/code_coverage/ingest_coverage/constants.js b/src/dev/code_coverage/ingest_coverage/constants.js index f2f467e461ae5..8f850ac2f1f12 100644 --- a/src/dev/code_coverage/ingest_coverage/constants.js +++ b/src/dev/code_coverage/ingest_coverage/constants.js @@ -27,8 +27,6 @@ export const RESEARCH_COVERAGE_INDEX = export const RESEARCH_TOTALS_INDEX = process.env.RESEARCH_TOTALS_INDEX || `qa_research_total_code_coverage`; -export const TEAM_ASSIGNMENT_PIPELINE_NAME = process.env.PIPELINE_NAME || 'team_assignment'; - export const CODE_COVERAGE_CI_JOB_NAME = 'elastic+kibana+code-coverage'; export const RESEARCH_CI_JOB_NAME = 'elastic+kibana+qa-research'; export const CI_JOB_NAME = process.env.COVERAGE_JOB_NAME || RESEARCH_CI_JOB_NAME; diff --git a/src/dev/code_coverage/ingest_coverage/either.js b/src/dev/code_coverage/ingest_coverage/either.js index eeb48893f18d8..326f238074e30 100644 --- a/src/dev/code_coverage/ingest_coverage/either.js +++ b/src/dev/code_coverage/ingest_coverage/either.js @@ -20,11 +20,15 @@ /* eslint new-cap: 0 */ /* eslint no-unused-vars: 0 */ +import { always } from './utils'; + export const Right = (x) => ({ chain: (f) => f(x), map: (f) => Right(f(x)), fold: (leftFn, rightFn) => rightFn(x), inspect: () => `Right(${x})`, + isLeft: always(false), + isRight: always(true), }); Right.of = function of(x) { @@ -40,6 +44,8 @@ export const Left = (x) => ({ map: (f) => Left(x), fold: (leftFn, rightFn) => leftFn(x), inspect: () => `Left(${x})`, + isLeft: always(true), + isRight: always(false), }); Left.of = function of(x) { diff --git a/src/dev/code_coverage/ingest_coverage/index.js b/src/dev/code_coverage/ingest_coverage/index.js index 4047ee78ee6ec..f29739c4cf29c 100644 --- a/src/dev/code_coverage/ingest_coverage/index.js +++ b/src/dev/code_coverage/ingest_coverage/index.js @@ -20,13 +20,16 @@ import { resolve } from 'path'; import { prok } from './process'; import { run, createFlagError } from '@kbn/dev-utils'; +import { pathExists } from './team_assignment/enumeration_helpers'; +import { id, reThrow } from './utils'; const ROOT = resolve(__dirname, '../../../..'); const flags = { - string: ['path', 'verbose', 'vcsInfoPath'], + string: ['path', 'verbose', 'vcsInfoPath', 'teamAssignmentsPath'], help: ` --path Required, path to the file to extract coverage data --vcsInfoPath Required, path to the git info file (branch, sha, author, & commit msg) +--teamAssignmentsPath Required, path to the team assignments data file `, }; @@ -36,12 +39,18 @@ export function runCoverageIngestionCli() { if (flags.path === '') throw createFlagError('please provide a single --path flag'); if (flags.vcsInfoPath === '') throw createFlagError('please provide a single --vcsInfoPath flag'); + if (flags.teamAssignmentsPath === '') + throw createFlagError('please provide a single --teamAssignments flag'); if (flags.verbose) log.verbose(`Verbose logging enabled`); const resolveRoot = resolve.bind(null, ROOT); const jsonSummaryPath = resolveRoot(flags.path); const vcsInfoFilePath = resolveRoot(flags.vcsInfoPath); - prok({ jsonSummaryPath, vcsInfoFilePath }, log); + const { teamAssignmentsPath } = flags; + + pathExists(teamAssignmentsPath).fold(reThrow, id); + + prok({ jsonSummaryPath, vcsInfoFilePath, teamAssignmentsPath }, log); }, { description: ` diff --git a/src/dev/code_coverage/ingest_coverage/ingest.js b/src/dev/code_coverage/ingest_coverage/ingest.js index 31a94d161a3cc..24da7a9338a46 100644 --- a/src/dev/code_coverage/ingest_coverage/ingest.js +++ b/src/dev/code_coverage/ingest_coverage/ingest.js @@ -19,7 +19,7 @@ const { Client } = require('@elastic/elasticsearch'); import { createFailError } from '@kbn/dev-utils'; -import { RESEARCH_CI_JOB_NAME, TEAM_ASSIGNMENT_PIPELINE_NAME } from './constants'; +import { RESEARCH_CI_JOB_NAME } from './constants'; import { errMsg, redact, whichIndex } from './ingest_helpers'; import { pretty, green } from './utils'; import { right, left } from './either'; @@ -34,14 +34,10 @@ const isResearchJob = process.env.COVERAGE_JOB_NAME === RESEARCH_CI_JOB_NAME ? t export const ingest = (log) => async (body) => { const isTotal = !!body.isTotal; const index = whichIndex(isResearchJob)(isTotal); - const isACoverageIndex = isTotal ? false : true; const stringified = pretty(body); - const pipeline = TEAM_ASSIGNMENT_PIPELINE_NAME; - const finalPayload = isACoverageIndex - ? { index, body: stringified, pipeline } - : { index, body: stringified }; + const finalPayload = { index, body: stringified }; const justLog = dontSendButLog(log); const doSendToIndex = doSend(index); @@ -77,11 +73,11 @@ async function send(logF, idx, redactedEsHostUrl, client, requestBody) { const sendMsg = (actuallySent, redactedEsHostUrl, payload) => { const { index, body } = payload; return `### ${actuallySent ? 'Sent' : 'Fake Sent'}: -${payload.pipeline ? `\t### Team Assignment Pipeline: ${green(payload.pipeline)}` : ''} ${redactedEsHostUrl ? `\t### ES Host: ${redactedEsHostUrl}` : ''} + \t### Index: ${green(index)} + \t### payload.body: ${body} -${process.env.NODE_ENV === 'integration_test' ? `ingest-pipe=>${payload.pipeline}` : ''} `; }; diff --git a/src/dev/code_coverage/ingest_coverage/ingest_helpers.js b/src/dev/code_coverage/ingest_coverage/ingest_helpers.js index 86bcf03977082..a7822a671887b 100644 --- a/src/dev/code_coverage/ingest_coverage/ingest_helpers.js +++ b/src/dev/code_coverage/ingest_coverage/ingest_helpers.js @@ -24,7 +24,6 @@ import { COVERAGE_INDEX, RESEARCH_COVERAGE_INDEX, RESEARCH_TOTALS_INDEX, - TEAM_ASSIGNMENT_PIPELINE_NAME, TOTALS_INDEX, } from './constants'; @@ -70,12 +69,6 @@ function color(whichColor) { }; } -export function maybeTeamAssign(isACoverageIndex, body) { - const doAddTeam = isACoverageIndex ? true : false; - const payload = doAddTeam ? { ...body, pipeline: TEAM_ASSIGNMENT_PIPELINE_NAME } : body; - return payload; -} - export function whichIndex(isResearchJob) { return (isTotal) => isTotal ? whichTotalsIndex(isResearchJob) : whichCoverageIndex(isResearchJob); diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js b/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js index ba73922ec508a..a4d07215efec1 100644 --- a/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js @@ -20,6 +20,7 @@ import { resolve } from 'path'; import execa from 'execa'; import expect from '@kbn/expect'; +import shell from 'shelljs'; const ROOT_DIR = resolve(__dirname, '../../../../..'); const MOCKS_DIR = resolve(__dirname, './mocks'); @@ -35,9 +36,14 @@ const env = { }; describe('Ingesting coverage', () => { + const teamAssignmentsPath = + 'src/dev/code_coverage/ingest_coverage/team_assignment/team_assignments.txt'; + const verboseArgs = [ 'scripts/ingest_coverage.js', '--verbose', + '--teamAssignmentsPath', + teamAssignmentsPath, '--vcsInfoPath', 'src/dev/code_coverage/ingest_coverage/integration_tests/mocks/VCS_INFO.txt', '--path', @@ -46,6 +52,21 @@ describe('Ingesting coverage', () => { const summaryPath = 'jest-combined/coverage-summary-manual-mix.json'; const resolved = resolve(MOCKS_DIR, summaryPath); + beforeAll(async () => { + const params = [ + 'scripts/generate_team_assignments.js', + '--src', + '.github/CODEOWNERS', + '--dest', + teamAssignmentsPath, + ]; + await execa(process.execPath, params, { cwd: ROOT_DIR, env }); + }); + + afterAll(() => { + shell.rm(teamAssignmentsPath); + }); + describe(`staticSiteUrl`, () => { let actualUrl = ''; const siteUrlRegex = /"staticSiteUrl":\s*(.+,)/; diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/CODEOWNERS b/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/CODEOWNERS new file mode 100644 index 0000000000000..1822c3fd95e34 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/CODEOWNERS @@ -0,0 +1,6 @@ +# GitHub CODEOWNERS definition +# Identify which groups will be pinged by changes to different parts of the codebase. +# For more info, see https://help.github.com/articles/about-codeowners/ + +# App +/x-pack/plugins/code/ @elastic/kibana-tre diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-possibly-n-teams.json b/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-possibly-n-teams.json new file mode 100644 index 0000000000000..9e66d8f5cb101 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-possibly-n-teams.json @@ -0,0 +1,28 @@ +{ + "/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana/src/plugins/charts/public/static/color_maps/color_maps.ts": { + "lines": { + "total": 4, + "covered": 4, + "skipped": 0, + "pct": 100 + }, + "functions": { + "total": 1, + "covered": 1, + "skipped": 0, + "pct": 100 + }, + "statements": { + "total": 4, + "covered": 4, + "skipped": 0, + "pct": 100 + }, + "branches": { + "total": 0, + "covered": 0, + "skipped": 0, + "pct": 100 + } + } +} diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-qa-research-job.json b/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-qa-research-job.json new file mode 100644 index 0000000000000..6e4d8ea954c2c --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-qa-research-job.json @@ -0,0 +1,28 @@ +{ + "/var/lib/jenkins/workspace/elastic+kibana+qa-research/kibana/x-pack/plugins/reporting/server/browsers/extract/unzip.js": { + "lines": { + "total": 4, + "covered": 4, + "skipped": 0, + "pct": 100 + }, + "functions": { + "total": 1, + "covered": 1, + "skipped": 0, + "pct": 100 + }, + "statements": { + "total": 4, + "covered": 4, + "skipped": 0, + "pct": 100 + }, + "branches": { + "total": 0, + "covered": 0, + "skipped": 0, + "pct": 100 + } + } +} diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js b/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js new file mode 100644 index 0000000000000..c666581ddb08c --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { resolve } from 'path'; +import execa from 'execa'; +import expect from '@kbn/expect'; +import shell from 'shelljs'; + +const ROOT_DIR = resolve(__dirname, '../../../../..'); +const MOCKS_DIR = resolve(__dirname, './mocks'); + +describe('Team Assignment', () => { + const teamAssignmentsPath = + 'src/dev/code_coverage/ingest_coverage/team_assignment/team_assignments.txt'; + const mockCodeOwners = 'CODEOWNERS'; + const resolved = resolve(MOCKS_DIR, mockCodeOwners); + + beforeAll(async () => { + const params = [ + 'scripts/generate_team_assignments.js', + '--src', + resolved, + '--dest', + teamAssignmentsPath, + ]; + await execa(process.execPath, params, { cwd: ROOT_DIR }); + }); + + afterAll(() => { + shell.rm(teamAssignmentsPath); + }); + + describe(`when the codeowners file contains #CC#`, () => { + it(`should strip the prefix and still drill down through the fs`, async () => { + const { stdout } = await execa('grep', ['tre', teamAssignmentsPath], { cwd: ROOT_DIR }); + expect(stdout).to.be(`x-pack/plugins/code/server/config.ts kibana-tre +x-pack/plugins/code/server/index.ts kibana-tre +x-pack/plugins/code/server/plugin.test.ts kibana-tre +x-pack/plugins/code/server/plugin.ts kibana-tre`); + }); + }); +}); diff --git a/src/dev/code_coverage/ingest_coverage/process.js b/src/dev/code_coverage/ingest_coverage/process.js index 85a42cfffa6e2..28a7c9ccd41b0 100644 --- a/src/dev/code_coverage/ingest_coverage/process.js +++ b/src/dev/code_coverage/ingest_coverage/process.js @@ -18,7 +18,7 @@ */ import { fromEventPattern, of, fromEvent } from 'rxjs'; -import { concatMap, delay, map, takeUntil } from 'rxjs/operators'; +import { concatMap, delay, map, mergeMap, takeUntil } from 'rxjs/operators'; import jsonStream from './json_stream'; import { pipe, noop, green, always } from './utils'; import { ingest } from './ingest'; @@ -32,6 +32,7 @@ import { coveredFilePath, ciRunUrl, itemizeVcs, + teamAssignment, } from './transforms'; import { resolve } from 'path'; import { createReadStream } from 'fs'; @@ -50,9 +51,10 @@ const addPrePopulatedTimeStamp = addTimeStamp(process.env.TIME_STAMP || formatte const preamble = pipe(statsAndstaticSiteUrl, rootDirAndOrigPath, buildId, addPrePopulatedTimeStamp); const addTestRunnerAndStaticSiteUrl = pipe(testRunner, staticSite(staticSiteUrlBase)); -const transform = (jsonSummaryPath) => (log) => (vcsInfo) => { +const transform = (jsonSummaryPath) => (log) => (vcsInfo) => (teamAssignmentsPath) => { const objStream = jsonStream(jsonSummaryPath).on('done', noop); const itemizeVcsInfo = itemizeVcs(vcsInfo); + const assignTeams = teamAssignment(teamAssignmentsPath)(log); const jsonSummary$ = (_) => objStream.on('node', '!.*', _); @@ -64,6 +66,7 @@ const transform = (jsonSummaryPath) => (log) => (vcsInfo) => { map(ciRunUrl), map(addJsonSummaryPath(jsonSummaryPath)), map(addTestRunnerAndStaticSiteUrl), + mergeMap(assignTeams), concatMap((x) => of(x).pipe(delay(ms))) ) .subscribe(ingest(log)); @@ -83,7 +86,7 @@ const vcsInfoLines$ = (vcsInfoFilePath) => { return fromEvent(rl, 'line').pipe(takeUntil(fromEvent(rl, 'close'))); }; -export const prok = ({ jsonSummaryPath, vcsInfoFilePath }, log) => { +export const prok = ({ jsonSummaryPath, vcsInfoFilePath, teamAssignmentsPath }, log) => { validateRoot(COVERAGE_INGESTION_KIBANA_ROOT, log); logAll(jsonSummaryPath, log); @@ -93,7 +96,7 @@ export const prok = ({ jsonSummaryPath, vcsInfoFilePath }, log) => { vcsInfoLines$(vcsInfoFilePath).subscribe( mutateVcsInfo(vcsInfo), (err) => log.error(err), - always(xformWithPath(vcsInfo)) + always(xformWithPath(vcsInfo)(teamAssignmentsPath)) ); }; diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/enumerate_patterns.js b/src/dev/code_coverage/ingest_coverage/team_assignment/enumerate_patterns.js new file mode 100644 index 0000000000000..dcdb32c91b8f8 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/enumerate_patterns.js @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { readdirSync, statSync } from 'fs'; +import { join } from 'path'; +import { + push, + prokGlob, + trim, + isRejectedDir, + isFileAllowed, + isDir, + tryPath, + dropEmpty, + notFound, +} from './enumeration_helpers'; +import { stripLeading } from '../transforms'; + +export const enumeratePatterns = (rootPath) => (log) => (patterns) => { + const res = []; + const resPush = push(res); + const logNotFound = notFound(log); + + for (const entry of patterns) { + const [pathPattern, team] = entry; + const cleaned = stripLeading(pathPattern); + const existsWithOwner = pathExists(team); + + const collect = (x) => existsWithOwner(x).forEach(resPush); + tryPath(cleaned).fold(logNotFound, collect); + } + + return res; + + function pathExists(owner) { + const creeper = (x) => creepFsSync(x, [], rootPath, owner); + return function creepAllAsGlobs(pathPattern) { + return prokGlob(pathPattern).map(creeper).filter(dropEmpty); + }; + } +}; + +function creepFsSync(aPath, xs, rootPath, owner) { + xs = xs || []; + + const joinRoot = join.bind(null, rootPath); + const trimRoot = trim(rootPath); + const joined = joinRoot(aPath); + const isADir = isDir(joined); + + (isADir ? readdirSync(joined) : [aPath]).forEach(maybeRecurse); + + return xs; + + function maybeRecurse(entry) { + const full = isADir ? join(aPath, entry) : entry; + const fullIsDir = statSync(full).isDirectory(); + + if (fullIsDir && !isRejectedDir(full)) xs = creepFsSync(full, xs, rootPath, owner); + else if (isFileAllowed(full)) xs.push(`${trimRoot(full)} ${owner}`); + } +} diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/enumeration_helpers.js b/src/dev/code_coverage/ingest_coverage/team_assignment/enumeration_helpers.js new file mode 100644 index 0000000000000..b06141965d2b6 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/enumeration_helpers.js @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { statSync } from 'fs'; +import isGlob from 'is-glob'; +import glob from 'glob'; +import { left, right, tryCatch } from '../either'; + +export const push = (xs) => (x) => xs.push(x); +export const pathExists = (x) => tryCatch(() => statSync(x)).fold(left, right); +export const isDir = (x) => statSync(x).isDirectory(); +export const prokGlob = (x) => glob.sync(x, { nonull: true }); +export const trim = (ROOT) => (x) => x.replace(`${ROOT}/`, ''); +export const isFileAllowed = (x) => { + const isJsOrTsOrTsxOrJsx = /.(j|t)(s|sx)$/gm; + return isJsOrTsOrTsxOrJsx.test(x); +}; +export const isRejectedDir = (x) => + /node_modules|__tests__|__fixture__|__fixtures__|build\//gm.test(x); +const isGlobFound = (x) => (xs) => (x === xs[0] ? false : true); +export const globExpands = (x) => isGlobFound(x)(prokGlob(x)); +export const tryPath = (x) => { + const isAGlob = isGlob(x); + + if (isAGlob) return globExpands(x) ? right(x) : left(x); + + if (!isAGlob) return pathExists(x).isRight() ? right(x) : left(x); +}; +export const dropEmpty = (x) => x.length > 0; +export const notFound = (log) => (err) => log.error(`\n!!! Not Found: \n${err}`); diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/update_ingest_pipeline.js b/src/dev/code_coverage/ingest_coverage/team_assignment/flush.js similarity index 51% rename from src/dev/code_coverage/ingest_coverage/team_assignment/update_ingest_pipeline.js rename to src/dev/code_coverage/ingest_coverage/team_assignment/flush.js index 22a9d0a461ebf..5150ac2e655f2 100644 --- a/src/dev/code_coverage/ingest_coverage/team_assignment/update_ingest_pipeline.js +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/flush.js @@ -17,21 +17,28 @@ * under the License. */ -import { createFailError } from '@kbn/dev-utils'; -import { ES_HOST } from '../constants'; -import { pretty, green } from '../utils'; +import { writeFileSync } from 'fs'; +import shell from 'shelljs'; +import { tryCatch } from '../either'; +import { id } from '../utils'; -const { Client } = require('@elastic/elasticsearch'); +const encoding = 'utf8'; +const appendUtf8 = { flag: 'a', encoding }; -const node = ES_HOST; -const client = new Client({ node }); +export const flush = (dest) => (log) => (assignments) => { + log.verbose(`\n### Flushing assignments to: \n\t${dest}`); -export const update = (id) => (log) => async (body) => { - try { - await client.ingest.putPipeline({ id, body }); - log.verbose(`### Ingestion Pipeline ID: ${green(id)}`); - log.verbose(`### Payload Partial: \n${body.slice(0, 600)}...`); - } catch (e) { - throw createFailError(`${pretty(e.meta)}`); - } + const writeToFile = writeFileSync.bind(null, dest); + + writeToFile('', { encoding }); + + for (const xs of assignments) xs.forEach((x) => writeToFile(`${x}\n`, appendUtf8)); + + tryCatch(() => maybeShowSize(dest)).fold(id, (x) => { + log.verbose(`\n### Flushed [${x}] lines`); + }); }; +function maybeShowSize(x) { + const { output } = shell.exec(`wc -l ${x}`, { silent: true }); + return output.match(/\s*\d*\s*/)[0].trim(); +} diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/index.js b/src/dev/code_coverage/ingest_coverage/team_assignment/index.js index 11f9748708283..30112df3b6ba4 100644 --- a/src/dev/code_coverage/ingest_coverage/team_assignment/index.js +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/index.js @@ -17,42 +17,55 @@ * under the License. */ -import { run } from '@kbn/dev-utils'; -import { TEAM_ASSIGNMENT_PIPELINE_NAME } from '../constants'; -import { fetch } from './get_data'; -import { update } from './update_ingest_pipeline'; +import { run, createFlagError, REPO_ROOT } from '@kbn/dev-utils'; +import { parse } from './parse_owners'; +import { flush } from './flush'; +import { enumeratePatterns } from './enumerate_patterns'; +import { pipe } from '../utils'; +import { reduce } from 'rxjs/operators'; -const updatePipeline = update(TEAM_ASSIGNMENT_PIPELINE_NAME); - -const execute = ({ flags, log }) => { - if (flags.verbose) log.verbose(`### Verbose logging enabled`); - - const logLeft = handleErr(log); - const updateAndLog = updatePipeline(log); - - const { path } = flags; - - fetch(path).fold(logLeft, updateAndLog); +const flags = { + string: ['src', 'dest'], + help: ` +--src Required, path to CODEOWNERS file. +--dest Required, destination path of the assignments. + `, }; -function handleErr(log) { - return (msg) => log.error(msg); -} +export const generateTeamAssignments = () => { + run( + ({ flags, log }) => { + if (flags.src === '') throw createFlagError('please provide a single --src flag'); + if (flags.dest === '') throw createFlagError('please provide a single --dest flag'); -const description = ` + const logCreepAndFlush = pipe( + logSuccess(flags.src, log), + enumeratePatterns(REPO_ROOT)(log), + flush(flags.dest)(log) + ); -Upload the latest team assignment pipeline def from src, -to the cluster. + parse(flags.src).pipe(reduce(toMap, new Map())).subscribe(logCreepAndFlush); + }, + { + description: ` - `; +Create a file defining the team assignments, + parsed from .github/CODEOWNERS -const flags = { - string: ['path', 'verbose'], - help: ` ---path Required, path to painless definition for team assignment. - `, + `, + flags, + } + ); }; -const usage = 'node scripts/load_team_assignment.js --verbose --path PATH_TO_PAINLESS_SCRIPT.json'; +function toMap(acc, x) { + acc.set(x[0], x[1][0]); + return acc; +} -export const uploadTeamAssignmentJson = () => run(execute, { description, flags, usage }); +function logSuccess(src, log) { + return (dataObj) => { + log.verbose(`\n### Parsing [${src}] Complete`); + return dataObj; + }; +} diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json b/src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json deleted file mode 100644 index 017d208133cdc..0000000000000 --- a/src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json +++ /dev/null @@ -1 +0,0 @@ -{"description":"Kibana code coverage team assignments","processors":[{"script":{"lang":"painless","source":"\n String path = ctx.coveredFilePath; \n if (path.indexOf('src/legacy/core_plugins/kibana/') == 0) {\n\n if (path.indexOf('src/legacy/core_plugins/kibana/common/utils') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/migrations') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/dashboard/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/dev_tools/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/discover/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/home') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/home/np_ready/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/local_application_service/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/lib') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/lib/management/saved_objects') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/import/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/export/') == 0) ctx.team = 'kibana-platform';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/core_plugins/') == 0) {\n\n if (path.indexOf('src/legacy/core_plugins/apm_oss/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/legacy/core_plugins/console_legacy') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/elasticsearch') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/embeddable_api/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/input_control_vis') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/interpreter/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana_react/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/newsfeed') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/region_map') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/legacy/core_plugins/status_page/public') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/testbed') == 0) ctx.team = 'kibana-platform';\n // else if (path.indexOf('src/legacy/core_plugins/tests_bundle/') == 0) ctx.team = 'kibana-platform';\n \n else if (path.indexOf('src/legacy/core_plugins/tile_map') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/legacy/core_plugins/timelion') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/ui_metric/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_tagcloud') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_vega') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_vislib/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/visualizations/') == 0) ctx.team = 'kibana-app-arch';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/server/') == 0) {\n\n if (path.indexOf('src/legacy/server/config/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/http/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/legacy/server/index_patterns/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/server/keystore/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/logging/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/pid/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/sample_data/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/server/sass/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/saved_objects/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/status/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/url_shortening/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/server/utils/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/warnings/') == 0) ctx.team = 'kibana-operations';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/ui') == 0) {\n\n if (path.indexOf('src/legacy/ui/public/field_editor') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/timefilter') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/management') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/state_management') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/ui/public/new_platform') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/plugin_discovery') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/chrome') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/notify') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/documentation_links') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/autoload') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/capabilities') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('src/legacy/ui/public/apm') == 0) ctx.team = 'apm-ui';\n\n } else if (path.indexOf('src/plugins/') == 0) {\n\n if (path.indexOf('src/plugins/advanced_settings/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/apm_oss/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/plugins/bfetch/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/charts/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/charts/public/static/color_maps') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/console/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/dashboard/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/data/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/dev_tools/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/discover/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/embeddable/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/es_ui_shared/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/expressions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/home/public') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/home/server/tutorials') == 0) ctx.team = 'observability';\n else if (path.indexOf('src/plugins/home/server/services/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/home/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/index_pattern_management/public/service') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/index_pattern_management/public') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/input_control_vis/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/inspector/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_legacy/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/kibana_react/public/code_editor') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('src/plugins/kibana_react/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_utils/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_usage_collection/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/legacy_export/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/maps_legacy/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/region_map/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/tile_map/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/timelion') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/navigation/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/newsfeed') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/saved_objects_management/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/saved_objects/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/share/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/status_page/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/telemetry') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/testbed/server/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/ui_actions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/usage_collection/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/vis_default_editor') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/vis_type') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/visualizations/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/visualize/') == 0) ctx.team = 'kibana-app';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('x-pack/legacy/') == 0) {\n\n if (path.indexOf('x-pack/legacy/plugins/actions/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/alerting/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/apm/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/legacy/plugins/beats_management/') == 0) ctx.team = 'beats';\n else if (path.indexOf('x-pack/legacy/plugins/canvas/') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('x-pack/legacy/plugins/cross_cluster_replication/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/dashboard_mode/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/legacy/plugins/encrypted_saved_objects/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/legacy/plugins/index_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/infra/') == 0) ctx.team = 'logs-metrics-ui';\n else if (path.indexOf('x-pack/legacy/plugins/ingest_manager/') == 0) ctx.team = 'ingest-management';\n else if (path.indexOf('x-pack/legacy/plugins/license_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/maps/') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/legacy/plugins/ml/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/legacy/plugins/monitoring/') == 0) ctx.team = 'stack-monitoring-ui';\n else if (path.indexOf('x-pack/legacy/plugins/reporting') == 0) ctx.team = 'kibana-reporting';\n else if (path.indexOf('x-pack/legacy/plugins/rollup/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/security/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/legacy/plugins/siem/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules') == 0) ctx.team = 'security-intelligence-analytics';\n else if (path.indexOf('x-pack/legacy/plugins/snapshot_restore/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/task_manager') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/triggers_actions_ui/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/upgrade_assistant/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/uptime') == 0) ctx.team = 'uptime';\n else if (path.indexOf('x-pack/legacy/plugins/xpack_main/server/') == 0) ctx.team = 'kibana-platform';\n\n else if (path.indexOf('x-pack/legacy/server/lib/create_router/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/server/lib/check_license/') == 0) ctx.team = 'es-ui'; \n else if (path.indexOf('x-pack/legacy/server/lib/') == 0) ctx.team = 'kibana-platform'; \n else ctx.team = 'unknown';\n\n } else if (path.indexOf('x-pack/plugins/') == 0) {\n\n if (path.indexOf('x-pack/plugins/actions/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/advanced_ui_actions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/alerts') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/alerting_builtins') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/apm/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/plugins/beats_management/') == 0) ctx.team = 'beats';\n else if (path.indexOf('x-pack/plugins/canvas/') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('x-pack/plugins/case') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/cloud/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/code/') == 0) ctx.team = 'code';\n else if (path.indexOf('x-pack/plugins/console_extensions/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/cross_cluster_replication/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/dashboard_enhanced') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/dashboard_mode') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/discover_enhanced') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/embeddable_enhanced') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/data_enhanced/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/drilldowns/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/encrypted_saved_objects/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/endpoint/') == 0) ctx.team = 'endpoint-app-team';\n else if (path.indexOf('x-pack/plugins/es_ui_shared/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/event_log/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/features/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/file_upload') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/plugins/global_search') == 0) ctx.team = 'kibana-platform';\n \n else if (path.indexOf('x-pack/plugins/graph/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/grokdebugger/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/index_lifecycle_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/index_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/infra/') == 0) ctx.team = 'logs-metrics-ui';\n else if (path.indexOf('x-pack/plugins/ingest_manager/') == 0) ctx.team = 'ingest-management';\n else if (path.indexOf('x-pack/plugins/ingest_pipelines/') == 0) ctx.team = 'es-ui';\n \n else if (path.indexOf('x-pack/plugins/lens/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/license_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/licensing/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/lists/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/logstash') == 0) ctx.team = 'logstash';\n else if (path.indexOf('x-pack/plugins/maps/') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/plugins/maps_legacy_licensing') == 0) ctx.team = 'maps';\n else if (path.indexOf('x-pack/plugins/ml/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/plugins/monitoring') == 0) ctx.team = 'stack-monitoring-ui';\n else if (path.indexOf('x-pack/plugins/observability/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/plugins/oss_telemetry/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('x-pack/plugins/painless_lab/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/remote_clusters/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/reporting') == 0) ctx.team = 'kibana-reporting';\n else if (path.indexOf('x-pack/plugins/rollup/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/searchprofiler/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/security/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/security_solution/') == 0) ctx.team = 'siem';\n \n else if (path.indexOf('x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules') == 0) ctx.team = 'security-intelligence-analytics';\n else if (path.indexOf('x-pack/plugins/siem/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/snapshot_restore/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/spaces/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/task_manager/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/telemetry_collection_xpack/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('x-pack/plugins/transform/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/plugins/translations/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('x-pack/plugins/triggers_actions_ui/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/upgrade_assistant/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/ui_actions_enhanced') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/uptime') == 0) ctx.team = 'uptime';\n \n else if (path.indexOf('x-pack/plugins/watcher/') == 0) ctx.team = 'es-ui';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('packages') == 0) {\n\n if (path.indexOf('packages/kbn-analytics/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('packages/kbn-babel') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-config-schema/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('packages/elastic-datemath') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('packages/kbn-dev-utils') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-es/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-eslint') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-expect') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('packages/kbn-interpreter/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('packages/kbn-optimizer/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-pm/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-test/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-test-subj-selector/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-ui-framework/') == 0) ctx.team = 'kibana-design';\n else if (path.indexOf('packages/kbn-ui-shared-deps/') == 0) ctx.team = 'kibana-operations';\n else ctx.team = 'unknown';\n\n } else {\n\n if (path.indexOf('config/kibana.yml') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/apm.js') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/core/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/core/public/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/core/server/csp/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('src/dev/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/dev/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/dev/run_check_published_api_changes.ts') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('packages/kbn-es-archiver/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/optimize/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/setup_node_env/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/test_utils/') == 0) ctx.team = 'kibana-operations'; \n else ctx.team = 'unknown';\n }"}}]} diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/get_data.js b/src/dev/code_coverage/ingest_coverage/team_assignment/parse_owners.js similarity index 59% rename from src/dev/code_coverage/ingest_coverage/team_assignment/get_data.js rename to src/dev/code_coverage/ingest_coverage/team_assignment/parse_owners.js index 34526a2f79302..a07d556c9b403 100644 --- a/src/dev/code_coverage/ingest_coverage/team_assignment/get_data.js +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/parse_owners.js @@ -17,17 +17,19 @@ * under the License. */ -import { readFileSync } from 'fs'; -import { resolve } from 'path'; -import { tryCatch as tc } from '../either'; +import { fromEvent } from 'rxjs'; +import { map, filter, takeUntil } from 'rxjs/operators'; +import { lineRead, pathAndTeams, empties, comments, dropCCDelim } from './parse_owners_helpers'; +import { pipe } from '../utils'; -const ROOT = resolve(__dirname, '../../../../..'); +const cleanAndParse = pipe(dropCCDelim, pathAndTeams); -const resolveFromRoot = resolve.bind(null, ROOT); +const allLines$ = (lineReader) => + fromEvent(lineReader, 'line').pipe( + filter(empties), + filter(comments), + map(cleanAndParse), + takeUntil(fromEvent(lineReader, 'close')) + ); -const resolved = (path) => () => resolveFromRoot(path); - -const getContents = (path) => tc(() => readFileSync(path, 'utf8')); - -// fetch :: String -> Left | Right -export const fetch = (path) => tc(resolved(path)).chain(getContents); +export const parse = (codeOwnersPath) => allLines$(lineRead(codeOwnersPath)); diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/parse_owners_helpers.js b/src/dev/code_coverage/ingest_coverage/team_assignment/parse_owners_helpers.js new file mode 100644 index 0000000000000..454accb00a7b6 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/parse_owners_helpers.js @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { always, id, pipe } from '../utils'; +import * as Either from '../either'; +import readline from 'readline'; +import { createReadStream } from 'fs'; +import { pluckIndex } from '../transforms'; + +const coverageDelimRe = /^#CC#\s/; + +export const empties = (x) => x !== ''; +export const comments = (x) => !/^#\s{1,3}/.test(x); +const dropDelim = (x) => () => x.replace(coverageDelimRe, ''); + +export const dropCCDelim = (x) => + Either.fromNullable(coverageDelimRe.test(x)).fold(always(x), id(dropDelim(x))); + +const splitFilter = (splitter) => (x) => x.split(splitter).filter(empties); +const spaceSplit = splitFilter(' '); +const esSplit = splitFilter('@elastic/'); +const getFirst = pluckIndex(0); +const trimEsGrabFirst = pipe(esSplit, getFirst); + +export const pathAndTeams = (x) => { + const [path, ...teamEntries] = spaceSplit(x); + const teams = teamEntries.map(trimEsGrabFirst); + + return [path, teams]; +}; + +export const lineRead = (x) => readline.createInterface({ input: createReadStream(x) }); diff --git a/examples/alerting_example/common/constants.ts b/src/dev/code_coverage/ingest_coverage/team_assignment/parsing_helpers.js similarity index 67% rename from examples/alerting_example/common/constants.ts rename to src/dev/code_coverage/ingest_coverage/team_assignment/parsing_helpers.js index 5884eb3220519..50dd6f719f34e 100644 --- a/examples/alerting_example/common/constants.ts +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/parsing_helpers.js @@ -17,18 +17,14 @@ * under the License. */ -export const ALERTING_EXAMPLE_APP_ID = 'AlertingExample'; +import { always } from '../utils'; +import * as Either from '../either'; -// always firing -export const DEFAULT_INSTANCES_TO_GENERATE = 5; +const coverageDelimRe = /^#CC#\s/; -// Astros -export enum Craft { - OuterSpace = 'Outer Space', - ISS = 'ISS', -} -export enum Operator { - AreAbove = 'Are above', - AreBelow = 'Are below', - AreExactly = 'Are exactly', -} +export const empties = (x) => x !== ''; +export const comments = (x) => !/^#\s{1,3}/.test(x); +const dropDelim = (x) => x.replace(coverageDelimRe, ''); + +export const dropCCDelim = (x) => + Either.fromNullable(coverageDelimRe.test(x)).fold(always(x), always(dropDelim(x))); diff --git a/src/dev/code_coverage/ingest_coverage/transforms.js b/src/dev/code_coverage/ingest_coverage/transforms.js index b8c9acd6fc49d..ff6ffd46462ed 100644 --- a/src/dev/code_coverage/ingest_coverage/transforms.js +++ b/src/dev/code_coverage/ingest_coverage/transforms.js @@ -18,8 +18,12 @@ */ import * as Either from './either'; -import { fromNullable } from './maybe'; -import { always, id, noop } from './utils'; +import * as Maybe from './maybe'; +import { always, id, noop, pink } from './utils'; +import execa from 'execa'; +import { resolve } from 'path'; + +const ROOT_DIR = resolve(__dirname, '../../../..'); const maybeTotal = (x) => (x === 'total' ? Either.left(x) : Either.right(x)); @@ -83,20 +87,60 @@ export const staticSite = (urlBase) => (obj) => { return { ...obj, staticSiteUrl: prokForBoth() }; }; +const leadingSlashRe = /^\//; +export const maybeDropLeadingSlash = (x) => + leadingSlashRe.test(x) ? Either.right(x) : Either.left(x); +export const dropLeadingSlash = (x) => x.replace(leadingSlashRe, ''); +export const stripLeading = (x) => maybeDropLeadingSlash(x).fold(id, dropLeadingSlash); + export const coveredFilePath = (obj) => { const { staticSiteUrl, COVERAGE_INGESTION_KIBANA_ROOT } = obj; const withoutCoveredFilePath = always(obj); - const leadingSlashRe = /^\//; - const maybeDropLeadingSlash = (x) => (leadingSlashRe.test(x) ? Either.right(x) : Either.left(x)); - const dropLeadingSlash = (x) => x.replace(leadingSlashRe, ''); - const dropRoot = (root) => (x) => - maybeDropLeadingSlash(x.replace(root, '')).fold(id, dropLeadingSlash); + const dropRoot = (root) => (x) => stripLeading(x.replace(root, '')); return maybeTotal(staticSiteUrl) .map(dropRoot(COVERAGE_INGESTION_KIBANA_ROOT)) .fold(withoutCoveredFilePath, (coveredFilePath) => ({ ...obj, coveredFilePath })); }; +const findTeam = (x) => x.match(/.+\s{1,3}(.+)$/, 'gm'); +export const pluckIndex = (idx) => (xs) => xs[idx]; +const pluckTeam = pluckIndex(1); + +export const teamAssignment = (teamAssignmentsPath) => (log) => async (obj) => { + const { coveredFilePath } = obj; + const isTotal = Either.fromNullable(obj.isTotal); + + return isTotal.isRight() ? obj : await assignTeam(teamAssignmentsPath, coveredFilePath, log, obj); +}; +export const last = (x) => { + const xs = x.split('\n'); + const len = xs.length; + + return len === 1 ? xs[0] : xs[len - 1]; +}; +async function assignTeam(teamAssignmentsPath, coveredFilePath, log, obj) { + const params = [coveredFilePath, teamAssignmentsPath]; + + let grepResponse; + + try { + const { stdout } = await execa('grep', params, { cwd: ROOT_DIR }); + grepResponse = stdout; + } catch (e) { + log.error(`\n!!! Unknown Team for path: \n\t\t${pink(coveredFilePath)}\n`); + } + + return Either.fromNullable(grepResponse) + .map(last) + .map(findTeam) + .map(pluckTeam) + .fold( + () => ({ team: 'unknown', ...obj }), + (team) => ({ team, ...obj }) + ); +} + export const ciRunUrl = (obj) => Either.fromNullable(process.env.CI_RUN_URL).fold(always(obj), (ciRunUrl) => ({ ...obj, @@ -126,13 +170,12 @@ export const itemizeVcs = (vcsInfo) => (obj) => { }; const mutateVcs = (x) => (vcs.commitMsg = truncateMsg(x)); - fromNullable(commitMsg).map(mutateVcs); + Maybe.fromNullable(commitMsg).map(mutateVcs); const vcsCompareUrl = process.env.FETCHED_PREVIOUS ? `${comparePrefix()}/${process.env.FETCHED_PREVIOUS}...${sha}` : 'PREVIOUS SHA NOT PROVIDED'; - // const withoutPreviousL = always({ ...obj, vcs }); const withPreviousR = () => ({ ...obj, vcs: { diff --git a/src/dev/code_coverage/ingest_coverage/utils.js b/src/dev/code_coverage/ingest_coverage/utils.js index 7d817bdf7a6f3..e854e3d3b7248 100644 --- a/src/dev/code_coverage/ingest_coverage/utils.js +++ b/src/dev/code_coverage/ingest_coverage/utils.js @@ -22,6 +22,10 @@ import chalk from 'chalk'; export const pipe = (...fns) => fns.reduce((f, g) => (...args) => g(f(...args))); export const noop = () => {}; export const green = (x) => chalk.greenBright.bold(x); +export const pink = (x) => chalk.bgMagenta.bold.cyan.bold(x); export const id = (x) => x; -export const always = (x) => () => x; +export const always = (x) => () => x; // Wraps a value in a fn. Eager evaluation if passed a fn. export const pretty = (x) => JSON.stringify(x, null, 2); +export const reThrow = (e) => { + throw e; +}; diff --git a/src/dev/code_coverage/shell_scripts/assign_teams.sh b/src/dev/code_coverage/shell_scripts/assign_teams.sh deleted file mode 100644 index aaa14655a9a26..0000000000000 --- a/src/dev/code_coverage/shell_scripts/assign_teams.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -echo "### Code Coverage Team Assignment" -echo "" - -PIPELINE_NAME=$1 -export PIPELINE_NAME - -ES_HOST="https://${USER_FROM_VAULT}:${PASS_FROM_VAULT}@${HOST_FROM_VAULT}" -export ES_HOST - -node scripts/load_team_assignment.js --verbose --path src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json - -echo "### Code Coverage Team Assignment - Complete" -echo "" diff --git a/src/dev/code_coverage/shell_scripts/ingest_coverage.sh b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh similarity index 78% rename from src/dev/code_coverage/shell_scripts/ingest_coverage.sh rename to src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh index 0b67dac307473..62b81929ae79b 100644 --- a/src/dev/code_coverage/shell_scripts/ingest_coverage.sh +++ b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh @@ -27,19 +27,24 @@ export STATIC_SITE_URL_BASE DELAY=100 export DELAY +TEAM_ASSIGN_PATH=$5 + +# Build team assignments dat file +node scripts/generate_team_assignments.js --verbose --src .github/CODEOWNERS --dest $TEAM_ASSIGN_PATH + for x in jest functional; do echo "### Ingesting coverage for ${x}" COVERAGE_SUMMARY_FILE=target/kibana-coverage/${x}-combined/coverage-summary.json - node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt + node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath $TEAM_ASSIGN_PATH done # Need to override COVERAGE_INGESTION_KIBANA_ROOT since mocha json file has original intake worker path COVERAGE_SUMMARY_FILE=target/kibana-coverage/mocha-combined/coverage-summary.json export COVERAGE_INGESTION_KIBANA_ROOT=/dev/shm/workspace/kibana -node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt +node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath $TEAM_ASSIGN_PATH echo "### Ingesting Code Coverage - Complete" echo "" diff --git a/src/fixtures/stubbed_logstash_index_pattern.js b/src/fixtures/stubbed_logstash_index_pattern.js index 5735b01eb3db4..5d6d254b9a000 100644 --- a/src/fixtures/stubbed_logstash_index_pattern.js +++ b/src/fixtures/stubbed_logstash_index_pattern.js @@ -17,10 +17,10 @@ * under the License. */ -import StubIndexPattern from 'test_utils/stub_index_pattern'; import stubbedLogstashFields from 'fixtures/logstash_fields'; import { getKbnFieldType } from '../plugins/data/common'; +import { getStubIndexPattern } from '../plugins/data/public/test_utils'; import { uiSettingsServiceMock } from '../core/public/ui_settings/ui_settings_service.mock'; const uiSettingSetupMock = uiSettingsServiceMock.createSetupContract(); @@ -46,7 +46,7 @@ export default function stubbedLogstashIndexPatternService() { }; }); - const indexPattern = new StubIndexPattern('logstash-*', (cfg) => cfg, 'time', fields, { + const indexPattern = getStubIndexPattern('logstash-*', (cfg) => cfg, 'time', fields, { uiSettings: uiSettingSetupMock, }); diff --git a/src/fixtures/telemetry_collectors/working_collector.ts b/src/fixtures/telemetry_collectors/working_collector.ts index bdf10b5e54919..0a3bf49638a7b 100644 --- a/src/fixtures/telemetry_collectors/working_collector.ts +++ b/src/fixtures/telemetry_collectors/working_collector.ts @@ -90,12 +90,15 @@ export const myCollector = makeUsageCollector({ type: { type: 'boolean' }, }, my_array: { - total: { - type: 'number', + type: 'array', + items: { + total: { + type: 'number', + }, + type: { type: 'boolean' }, }, - type: { type: 'boolean' }, }, - my_str_array: { type: 'keyword' }, + my_str_array: { type: 'array', items: { type: 'keyword' } }, my_index_signature_prop: { count: { type: 'number' }, avg: { type: 'number' }, diff --git a/src/plugins/advanced_settings/public/index.ts b/src/plugins/advanced_settings/public/index.ts index db478fa1579e6..0e621e7cd7d1a 100644 --- a/src/plugins/advanced_settings/public/index.ts +++ b/src/plugins/advanced_settings/public/index.ts @@ -17,11 +17,19 @@ * under the License. */ +import React from 'react'; import { PluginInitializerContext } from 'kibana/public'; import { AdvancedSettingsPlugin } from './plugin'; export { AdvancedSettingsSetup, AdvancedSettingsStart } from './types'; export { ComponentRegistry } from './component_registry'; -export { Field } from './management_app/components/field'; + +/** + * Exports the field component as a React.lazy component. We're explicitly naming it lazy here + * so any plugin that would import that can clearly see it's lazy loaded and can only be used + * inside a suspense context. + */ +const LazyField = React.lazy(() => import('./management_app/components/field')); +export { LazyField }; export function plugin(initializerContext: PluginInitializerContext) { return new AdvancedSettingsPlugin(); diff --git a/src/plugins/advanced_settings/public/management_app/components/field/index.ts b/src/plugins/advanced_settings/public/management_app/components/field/index.ts index d1b9b34515532..c486dc96bfc33 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/index.ts +++ b/src/plugins/advanced_settings/public/management_app/components/field/index.ts @@ -18,3 +18,6 @@ */ export { Field, getEditableValue } from './field'; + +// eslint-disable-next-line import/no-default-export +export { Field as default } from './field'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index 91286a38f16a0..bfb5fd9da56e6 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -179,8 +179,7 @@ describe('IndexPattern', () => { await indexPattern.addScriptedField( scriptedField.name, scriptedField.script, - scriptedField.type, - 'lang' + scriptedField.type ); const scriptedFields = indexPattern.getScriptedFields(); @@ -206,7 +205,7 @@ describe('IndexPattern', () => { const scriptedField = last(scriptedFields) as any; expect.assertions(1); try { - await indexPattern.addScriptedField(scriptedField.name, "'new script'", 'string', 'lang'); + await indexPattern.addScriptedField(scriptedField.name, "'new script'", 'string'); } catch (e) { expect(e).toBeInstanceOf(DuplicateField); } diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index 882235889b55c..e880c3a9ff825 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -234,12 +234,7 @@ export class IndexPattern implements IIndexPattern { * @param fieldType * @param lang */ - async addScriptedField( - name: string, - script: string, - fieldType: string = 'string', - lang: string = 'painless' - ) { + async addScriptedField(name: string, script: string, fieldType: string = 'string') { const scriptedFields = this.getScriptedFields(); const names = _.map(scriptedFields, 'name'); @@ -252,7 +247,7 @@ export class IndexPattern implements IIndexPattern { script, type: fieldType, scripted: true, - lang, + lang: 'painless', aggregatable: true, searchable: true, count: 0, @@ -322,13 +317,9 @@ export class IndexPattern implements IIndexPattern { return timeField && timeField.esTypes && timeField.esTypes.indexOf('date_nanos') !== -1; } - isTimeBasedWildcard(): boolean { - return this.isTimeBased() && this.isWildcard(); - } - getTimeField() { if (!this.timeFieldName || !this.fields || !this.fields.getByName) return undefined; - return this.fields.getByName(this.timeFieldName) || undefined; + return this.fields.getByName(this.timeFieldName); } getFieldByName(name: string): IndexPatternField | undefined { @@ -340,13 +331,6 @@ export class IndexPattern implements IIndexPattern { return this.typeMeta?.aggs; } - /** - * Does this index pattern title include a '*' - */ - private isWildcard() { - return _.includes(this.title, '*'); - } - /** * Returns index pattern as saved object body for saving */ diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index f7dceffa9fdbc..0e21f6f695551 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -365,8 +365,6 @@ export { ISearchGeneric, ISearchSource, parseSearchSourceJSON, - RequestTimeoutError, - SearchError, SearchInterceptor, SearchInterceptorDeps, SearchRequest, @@ -375,6 +373,11 @@ export { // expression functions and types EsdslExpressionFunctionDefinition, EsRawResponseExpressionTypeDefinition, + // errors + SearchError, + SearchTimeoutError, + TimeoutErrorMode, + PainlessError, } from './search'; export type { SearchSource } from './search'; diff --git a/src/plugins/data/public/index_patterns/index_pattern.stub.ts b/src/plugins/data/public/index_patterns/index_pattern.stub.ts new file mode 100644 index 0000000000000..e5c6c008e3e28 --- /dev/null +++ b/src/plugins/data/public/index_patterns/index_pattern.stub.ts @@ -0,0 +1,132 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import sinon from 'sinon'; + +import { CoreSetup } from 'src/core/public'; +import { FieldFormat as FieldFormatImpl } from '../../common/field_formats'; +import { IFieldType, FieldSpec } from '../../common/index_patterns'; +import { FieldFormatsStart } from '../field_formats'; +import { IndexPattern, indexPatterns, KBN_FIELD_TYPES, fieldList } from '../'; +import { getFieldFormatsRegistry } from '../test_utils'; +import { setFieldFormats } from '../services'; + +setFieldFormats(({ + getDefaultInstance: () => + ({ + getConverterFor: () => (value: any) => value, + convert: (value: any) => JSON.stringify(value), + } as FieldFormatImpl), +} as unknown) as FieldFormatsStart); + +export function getStubIndexPattern( + pattern: string, + getConfig: (cfg: any) => any, + timeField: string | null, + fields: FieldSpec[] | IFieldType[], + core: CoreSetup +): IndexPattern { + return (new StubIndexPattern( + pattern, + getConfig, + timeField, + fields, + core + ) as unknown) as IndexPattern; +} + +export class StubIndexPattern { + id: string; + title: string; + popularizeField: Function; + timeFieldName: string | null; + isTimeBased: () => boolean; + getConfig: (cfg: any) => any; + getNonScriptedFields: Function; + getScriptedFields: Function; + getFieldByName: Function; + getSourceFiltering: Function; + metaFields: string[]; + fieldFormatMap: Record; + getComputedFields: Function; + flattenHit: Function; + formatHit: Record; + fieldsFetcher: Record; + formatField: Function; + getFormatterForField: () => { convert: Function }; + _reindexFields: Function; + stubSetFieldFormat: Function; + fields?: FieldSpec[]; + + constructor( + pattern: string, + getConfig: (cfg: any) => any, + timeField: string | null, + fields: FieldSpec[] | IFieldType[], + core: CoreSetup + ) { + const registeredFieldFormats = getFieldFormatsRegistry(core); + + this.id = pattern; + this.title = pattern; + this.popularizeField = sinon.stub(); + this.timeFieldName = timeField; + this.isTimeBased = () => Boolean(this.timeFieldName); + this.getConfig = getConfig; + this.getNonScriptedFields = sinon.spy(IndexPattern.prototype.getNonScriptedFields); + this.getScriptedFields = sinon.spy(IndexPattern.prototype.getScriptedFields); + this.getFieldByName = sinon.spy(IndexPattern.prototype.getFieldByName); + this.getSourceFiltering = sinon.stub(); + this.metaFields = ['_id', '_type', '_source']; + this.fieldFormatMap = {}; + + this.getComputedFields = IndexPattern.prototype.getComputedFields.bind(this); + this.flattenHit = indexPatterns.flattenHitWrapper( + (this as unknown) as IndexPattern, + this.metaFields + ); + this.formatHit = indexPatterns.formatHitProvider( + (this as unknown) as IndexPattern, + registeredFieldFormats.getDefaultInstance(KBN_FIELD_TYPES.STRING) + ); + this.fieldsFetcher = { apiClient: { baseUrl: '' } }; + this.formatField = this.formatHit.formatField; + this.getFormatterForField = () => ({ + convert: () => '', + }); + + this._reindexFields = function () { + this.fields = fieldList((this.fields || fields) as FieldSpec[], false); + }; + + this.stubSetFieldFormat = function ( + fieldName: string, + id: string, + params: Record + ) { + const FieldFormat = registeredFieldFormats.getType(id); + this.fieldFormatMap[fieldName] = new FieldFormat!(params); + this._reindexFields(); + }; + + this._reindexFields(); + + return this; + } +} diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 6b419f6995447..1ee453a0f1411 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -8,6 +8,7 @@ import { $Values } from '@kbn/utility-types'; import _ from 'lodash'; import { Action } from 'history'; import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; +import { ApplicationStart } from 'kibana/public'; import { Assign } from '@kbn/utility-types'; import { BehaviorSubject } from 'rxjs'; import Boom from 'boom'; @@ -69,7 +70,6 @@ import { SavedObjectsClientContract } from 'src/core/public'; import { Search } from '@elastic/elasticsearch/api/requestParams'; import { SearchResponse } from 'elasticsearch'; import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common'; -import { Subscription } from 'rxjs'; import { ToastInputFields } from 'src/core/public/notifications'; import { ToastsSetup } from 'kibana/public'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; @@ -1086,7 +1086,7 @@ export type IMetricAggType = MetricAggType; export class IndexPattern implements IIndexPattern { // Warning: (ae-forgotten-export) The symbol "IndexPatternDeps" needs to be exported by the entry point index.d.ts constructor({ spec, savedObjectsClient, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps); - addScriptedField(name: string, script: string, fieldType?: string, lang?: string): Promise; + addScriptedField(name: string, script: string, fieldType?: string): Promise; // (undocumented) fieldFormatMap: Record; // (undocumented) @@ -1159,8 +1159,6 @@ export class IndexPattern implements IIndexPattern { // (undocumented) isTimeBased(): boolean; // (undocumented) - isTimeBasedWildcard(): boolean; - // (undocumented) isTimeNanosBased(): boolean; // (undocumented) metaFields: string[]; @@ -1464,6 +1462,8 @@ export interface ISearchStart { aggs: AggsStart; search: ISearchGeneric; searchSource: ISearchStartSearchSource; + // (undocumented) + showError: (e: Error) => void; } // @public @@ -1630,6 +1630,19 @@ export interface OptionedValueProp { value: string; } +// Warning: (ae-forgotten-export) The symbol "KbnError" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "PainlessError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class PainlessError extends KbnError { + // Warning: (ae-forgotten-export) The symbol "EsError" needs to be exported by the entry point index.d.ts + constructor(err: EsError, request: IKibanaSearchRequest); + // (undocumented) + getErrorMessage(application: ApplicationStart): JSX.Element; + // (undocumented) + painlessStack?: string; +} + // Warning: (ae-forgotten-export) The symbol "parseEsInterval" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ParsedInterval" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1895,13 +1908,6 @@ export interface RefreshInterval { value: number; } -// Warning: (ae-missing-release-tag) "RequestTimeoutError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export class RequestTimeoutError extends Error { - constructor(message?: string); -} - // Warning: (ae-missing-release-tag) "SavedQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2025,24 +2031,27 @@ export class SearchInterceptor { protected application: CoreStart['application']; // (undocumented) protected readonly deps: SearchInterceptorDeps; + // (undocumented) + protected getTimeoutMode(): TimeoutErrorMode; + // (undocumented) + protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, appAbortSignal?: AbortSignal): Error; // @internal protected pendingCount$: BehaviorSubject; // @internal (undocumented) - protected runSearch(request: IEsSearchRequest, signal: AbortSignal, strategy?: string): Observable; - search(request: IEsSearchRequest, options?: ISearchOptions): Observable; + protected runSearch(request: IKibanaSearchRequest, signal: AbortSignal, strategy?: string): Observable; + search(request: IKibanaSearchRequest, options?: ISearchOptions): Observable; // @internal (undocumented) protected setupAbortSignal({ abortSignal, timeout, }: { abortSignal?: AbortSignal; timeout?: number; }): { combinedSignal: AbortSignal; + timeoutSignal: AbortSignal; cleanup: () => void; }; // (undocumented) - protected showTimeoutError: ((e: Error) => void) & import("lodash").Cancelable; - // @internal - protected timeoutSubscriptions: Subscription; -} + showError(e: Error): void; + } // Warning: (ae-missing-release-tag) "SearchInterceptorDeps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2155,6 +2164,17 @@ export interface SearchSourceFields { version?: boolean; } +// Warning: (ae-missing-release-tag) "SearchTimeoutError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export class SearchTimeoutError extends KbnError { + constructor(err: Error, mode: TimeoutErrorMode); + // (undocumented) + getErrorMessage(application: ApplicationStart): JSX.Element; + // (undocumented) + mode: TimeoutErrorMode; + } + // Warning: (ae-missing-release-tag) "SortDirection" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2227,6 +2247,18 @@ export class TimeHistory { // @public (undocumented) export type TimeHistoryContract = PublicMethodsOf; +// Warning: (ae-missing-release-tag) "TimeoutErrorMode" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export enum TimeoutErrorMode { + // (undocumented) + CHANGE = 2, + // (undocumented) + CONTACT = 1, + // (undocumented) + UPGRADE = 0 +} + // Warning: (ae-missing-release-tag) "TimeRange" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2316,21 +2348,21 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:397:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:404:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:388:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:388:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:388:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:388:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:415:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/discover/public/application/components/fetch_error/index.ts b/src/plugins/data/public/search/errors/index.ts similarity index 92% rename from src/plugins/discover/public/application/components/fetch_error/index.ts rename to src/plugins/data/public/search/errors/index.ts index 0206bc48257ac..6082e758a8bad 100644 --- a/src/plugins/discover/public/application/components/fetch_error/index.ts +++ b/src/plugins/data/public/search/errors/index.ts @@ -17,4 +17,5 @@ * under the License. */ -import './fetch_error'; +export * from './painless_error'; +export * from './timeout_error'; diff --git a/src/plugins/data/public/search/errors/painless_error.tsx b/src/plugins/data/public/search/errors/painless_error.tsx new file mode 100644 index 0000000000000..244f205469a2f --- /dev/null +++ b/src/plugins/data/public/search/errors/painless_error.tsx @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiSpacer, EuiText, EuiCodeBlock } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ApplicationStart } from 'kibana/public'; +import { KbnError } from '../../../../kibana_utils/common'; +import { EsError, isEsError } from './types'; +import { IKibanaSearchRequest } from '..'; + +export class PainlessError extends KbnError { + painlessStack?: string; + constructor(err: EsError, request: IKibanaSearchRequest) { + const rootCause = getRootCause(err as EsError); + + super( + i18n.translate('data.painlessError.painlessScriptedFieldErrorMessage', { + defaultMessage: "Error executing Painless script: '{script}'.", + values: { script: rootCause?.script }, + }) + ); + this.painlessStack = rootCause?.script_stack ? rootCause?.script_stack.join('\n') : undefined; + } + + public getErrorMessage(application: ApplicationStart) { + function onClick() { + application.navigateToApp('management', { + path: `/kibana/indexPatterns`, + }); + } + + return ( + <> + {this.message} + + + {this.painlessStack ? ( + + {this.painlessStack} + + ) : null} + + + + + + + ); + } +} + +function getFailedShards(err: EsError) { + const failedShards = + err.body?.attributes?.error?.failed_shards || + err.body?.attributes?.error?.caused_by?.failed_shards; + return failedShards ? failedShards[0] : undefined; +} + +function getRootCause(err: EsError) { + return getFailedShards(err)?.reason; +} + +export function isPainlessError(err: Error | EsError) { + if (!isEsError(err)) return false; + + const rootCause = getRootCause(err as EsError); + if (!rootCause) return false; + + const { lang } = rootCause; + return lang === 'painless'; +} diff --git a/src/plugins/data/public/search/errors/timeout_error.test.tsx b/src/plugins/data/public/search/errors/timeout_error.test.tsx new file mode 100644 index 0000000000000..87b491b976ebc --- /dev/null +++ b/src/plugins/data/public/search/errors/timeout_error.test.tsx @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SearchTimeoutError, TimeoutErrorMode } from './timeout_error'; + +import { coreMock } from '../../../../../core/public/mocks'; +const startMock = coreMock.createStart(); + +import { mount } from 'enzyme'; +import { AbortError } from 'src/plugins/data/common'; + +describe('SearchTimeoutError', () => { + beforeEach(() => { + jest.clearAllMocks(); + startMock.application.navigateToApp.mockImplementation(jest.fn()); + }); + + it('Should navigate to upgrade', () => { + const e = new SearchTimeoutError(new AbortError(), TimeoutErrorMode.UPGRADE); + const component = mount(e.getErrorMessage(startMock.application)); + + expect(component.find('EuiButton').length).toBe(1); + component.find('EuiButton').simulate('click'); + expect(startMock.application.navigateToApp).toHaveBeenCalledWith('management', { + path: '/kibana/indexPatterns', + }); + }); + + it('Should create contact admin message', () => { + const e = new SearchTimeoutError(new AbortError(), TimeoutErrorMode.CONTACT); + const component = mount(e.getErrorMessage(startMock.application)); + + expect(component.find('EuiButton').length).toBe(0); + }); + + it('Should navigate to settings', () => { + const e = new SearchTimeoutError(new AbortError(), TimeoutErrorMode.CHANGE); + const component = mount(e.getErrorMessage(startMock.application)); + + expect(component.find('EuiButton').length).toBe(1); + component.find('EuiButton').simulate('click'); + expect(startMock.application.navigateToApp).toHaveBeenCalledWith('management', { + path: '/kibana/settings', + }); + }); +}); diff --git a/src/plugins/data/public/search/errors/timeout_error.tsx b/src/plugins/data/public/search/errors/timeout_error.tsx new file mode 100644 index 0000000000000..56aecb42f5326 --- /dev/null +++ b/src/plugins/data/public/search/errors/timeout_error.tsx @@ -0,0 +1,111 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; +import { ApplicationStart } from 'kibana/public'; +import { KbnError } from '../../../../kibana_utils/common'; + +export enum TimeoutErrorMode { + UPGRADE, + CONTACT, + CHANGE, +} + +/** + * Request Failure - When an entire multi request fails + * @param {Error} err - the Error that came back + */ +export class SearchTimeoutError extends KbnError { + public mode: TimeoutErrorMode; + constructor(err: Error, mode: TimeoutErrorMode) { + super(`Request timeout: ${JSON.stringify(err?.message)}`); + this.mode = mode; + } + + private getMessage() { + switch (this.mode) { + case TimeoutErrorMode.UPGRADE: + return i18n.translate('data.search.upgradeLicense', { + defaultMessage: + 'Your query has timed out. With our free Basic tier, your queries never time out.', + }); + case TimeoutErrorMode.CONTACT: + return i18n.translate('data.search.timeoutContactAdmin', { + defaultMessage: + 'Your query has timed out. Contact your system administrator to increase the run time.', + }); + case TimeoutErrorMode.CHANGE: + return i18n.translate('data.search.timeoutIncreaseSetting', { + defaultMessage: + 'Your query has timed out. Increase run time with the search timeout advanced setting.', + }); + } + } + + private getActionText() { + switch (this.mode) { + case TimeoutErrorMode.UPGRADE: + return i18n.translate('data.search.upgradeLicenseActionText', { + defaultMessage: 'Upgrade now', + }); + break; + case TimeoutErrorMode.CHANGE: + return i18n.translate('data.search.timeoutIncreaseSettingActionText', { + defaultMessage: 'Edit setting', + }); + break; + } + } + + private onClick(application: ApplicationStart) { + switch (this.mode) { + case TimeoutErrorMode.UPGRADE: + application.navigateToApp('management', { + path: `/kibana/indexPatterns`, + }); + break; + case TimeoutErrorMode.CHANGE: + application.navigateToApp('management', { + path: `/kibana/settings`, + }); + break; + } + } + + public getErrorMessage(application: ApplicationStart) { + const actionText = this.getActionText(); + return ( + <> + {this.getMessage()} + {actionText && ( + <> + + + this.onClick(application)} size="s"> + {actionText} + + + + )} + + ); + } +} diff --git a/src/plugins/discover/public/application/angular/get_painless_error.ts b/src/plugins/data/public/search/errors/types.ts similarity index 61% rename from src/plugins/discover/public/application/angular/get_painless_error.ts rename to src/plugins/data/public/search/errors/types.ts index 162dacd3ac3b7..4182209eb68a5 100644 --- a/src/plugins/discover/public/application/angular/get_painless_error.ts +++ b/src/plugins/data/public/search/errors/types.ts @@ -17,9 +17,7 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; - -interface FailedShards { +interface FailedShard { shard: number; index: string; node: string; @@ -41,7 +39,7 @@ interface FailedShards { }; } -interface EsError { +export interface EsError { body: { statusCode: number; error: string; @@ -56,51 +54,20 @@ interface EsError { ]; type: string; reason: string; + failed_shards: FailedShard[]; caused_by: { type: string; reason: string; phase: string; grouped: boolean; - failed_shards: FailedShards[]; + failed_shards: FailedShard[]; + script_stack: string[]; }; }; }; }; } -export function getCause(error: EsError) { - const cause = error.body?.attributes?.error?.root_cause; - if (cause) { - return cause[0]; - } - - const failedShards = error.body?.attributes?.error?.caused_by?.failed_shards; - - if (failedShards && failedShards[0] && failedShards[0].reason) { - return error.body?.attributes?.error?.caused_by?.failed_shards[0].reason; - } -} - -export function getPainlessError(error: EsError) { - const cause = getCause(error); - - if (!cause) { - return; - } - - const { lang, script } = cause; - - if (lang !== 'painless') { - return; - } - - return { - lang, - script, - message: i18n.translate('discover.painlessError.painlessScriptedFieldErrorMessage', { - defaultMessage: "Error with Painless scripted field '{script}'.", - values: { script }, - }), - error: error.body?.message, - }; +export function isEsError(e: any): e is EsError { + return !!e.body?.attributes; } diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index fc3d71936a859..86804a819cb0e 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -46,4 +46,4 @@ export { export { getEsPreference } from './es_search'; export { SearchInterceptor, SearchInterceptorDeps } from './search_interceptor'; -export { RequestTimeoutError } from './request_timeout_error'; +export * from './errors'; diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index fdd6a90013413..e931b39eae2a5 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -34,6 +34,7 @@ function createStartContract(): jest.Mocked { return { aggs: searchAggsStartMock(), search: jest.fn(), + showError: jest.fn(), searchSource: searchSourceMock, }; } diff --git a/src/plugins/data/public/search/request_timeout_error.ts b/src/plugins/data/public/search/request_timeout_error.ts deleted file mode 100644 index 92894deb4f0ff..0000000000000 --- a/src/plugins/data/public/search/request_timeout_error.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Class used to signify that a request timed out. Useful for applications to conditionally handle - * this type of error differently than other errors. - */ -export class RequestTimeoutError extends Error { - constructor(message = 'Request timed out') { - super(message); - this.message = message; - this.name = 'RequestTimeoutError'; - } -} diff --git a/src/plugins/data/public/search/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor.test.ts index 7bfa6f0ab1bc5..ade15adc1c3a3 100644 --- a/src/plugins/data/public/search/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor.test.ts @@ -22,6 +22,7 @@ import { coreMock } from '../../../../core/public/mocks'; import { IEsSearchRequest } from '../../common/search'; import { SearchInterceptor } from './search_interceptor'; import { AbortError } from '../../common'; +import { SearchTimeoutError, PainlessError } from './errors'; let searchInterceptor: SearchInterceptor; let mockCoreSetup: MockedKeys; @@ -53,8 +54,8 @@ describe('SearchInterceptor', () => { expect(result).toBe(mockResponse); }); - test('Observable should fail if fetch has an error', async () => { - const mockResponse: any = { result: 500 }; + test('Observable should fail if fetch has an internal error', async () => { + const mockResponse: any = { result: 500, message: 'Internal Error' }; mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); const mockRequest: IEsSearchRequest = { params: {}, @@ -68,64 +69,83 @@ describe('SearchInterceptor', () => { } }); - test('Observable should fail if fetch times out (test merged signal)', async () => { - mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => { - return new Promise((resolve, reject) => { - options.signal.addEventListener('abort', () => { - reject(new AbortError()); - }); - - setTimeout(resolve, 5000); - }); - }); + test('Should throw SearchTimeoutError on server timeout AND show toast', async (done) => { + const mockResponse: any = { + result: 500, + body: { + message: 'Request timed out', + }, + }; + mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); const mockRequest: IEsSearchRequest = { params: {}, }; const response = searchInterceptor.search(mockRequest); - const next = jest.fn(); - const error = (e: any) => { - expect(next).not.toBeCalled(); - expect(e).toBeInstanceOf(AbortError); - }; - response.subscribe({ next, error }); - - jest.advanceTimersByTime(5000); - - await flushPromises(); + try { + await response.toPromise(); + } catch (e) { + expect(e).toBeInstanceOf(SearchTimeoutError); + expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1); + done(); + } }); - test('Should not timeout if requestTimeout is undefined', async () => { - searchInterceptor = new SearchInterceptor({ - startServices: mockCoreSetup.getStartServices(), - uiSettings: mockCoreSetup.uiSettings, - http: mockCoreSetup.http, - toasts: mockCoreSetup.notifications.toasts, - }); - mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => { - return new Promise((resolve, reject) => { - options.signal.addEventListener('abort', () => { - reject(new AbortError()); - }); - - setTimeout(resolve, 5000); - }); - }); + test('Search error should be debounced', async (done) => { + const mockResponse: any = { + result: 500, + body: { + message: 'Request timed out', + }, + }; + mockCoreSetup.http.fetch.mockRejectedValue(mockResponse); const mockRequest: IEsSearchRequest = { params: {}, }; - const response = searchInterceptor.search(mockRequest); + try { + await searchInterceptor.search(mockRequest).toPromise(); + } catch (e) { + expect(e).toBeInstanceOf(SearchTimeoutError); + try { + await searchInterceptor.search(mockRequest).toPromise(); + } catch (e2) { + expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1); + done(); + } + } + }); - expect.assertions(1); - const next = jest.fn(); - const complete = () => { - expect(next).toBeCalled(); + test('Should throw Painless error on server error with OSS format', async (done) => { + const mockResponse: any = { + result: 500, + body: { + attributes: { + error: { + failed_shards: [ + { + reason: { + lang: 'painless', + script_stack: ['a', 'b'], + reason: 'banana', + }, + }, + ], + }, + }, + }, }; - response.subscribe({ next, complete }); - - jest.advanceTimersByTime(5000); + mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + const response = searchInterceptor.search(mockRequest); - await flushPromises(); + try { + await response.toPromise(); + } catch (e) { + expect(e).toBeInstanceOf(PainlessError); + done(); + } }); test('Observable should fail if user aborts (test merged signal)', async () => { diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 802ee6db9433e..2e42635a7f811 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -17,29 +17,21 @@ * under the License. */ -import { trimEnd, debounce } from 'lodash'; -import { - BehaviorSubject, - throwError, - timer, - Subscription, - defer, - from, - Observable, - NEVER, -} from 'rxjs'; +import { get, trimEnd, debounce } from 'lodash'; +import { BehaviorSubject, throwError, timer, defer, from, Observable, NEVER } from 'rxjs'; import { catchError, finalize } from 'rxjs/operators'; import { CoreStart, CoreSetup, ToastsSetup } from 'kibana/public'; -import { i18n } from '@kbn/i18n'; import { getCombinedSignal, AbortError, - IEsSearchRequest, + IKibanaSearchRequest, IKibanaSearchResponse, ISearchOptions, ES_SEARCH_STRATEGY, } from '../../common'; import { SearchUsageCollector } from './collectors'; +import { SearchTimeoutError, PainlessError, isPainlessError, TimeoutErrorMode } from './errors'; +import { toMountPoint } from '../../../kibana_react/public'; export interface SearchInterceptorDeps { http: CoreSetup['http']; @@ -62,12 +54,6 @@ export class SearchInterceptor { */ protected pendingCount$ = new BehaviorSubject(0); - /** - * The subscriptions from scheduling the automatic timeout for each request. - * @internal - */ - protected timeoutSubscriptions: Subscription = new Subscription(); - /** * @internal */ @@ -84,11 +70,46 @@ export class SearchInterceptor { }); } + /* + * @returns `TimeoutErrorMode` indicating what action should be taken in case of a request timeout based on license and permissions. + * @internal + */ + protected getTimeoutMode() { + return TimeoutErrorMode.UPGRADE; + } + + /* + * @returns `Error` a search service specific error or the original error, if a specific error can't be recognized. + * @internal + */ + protected handleSearchError( + e: any, + request: IKibanaSearchRequest, + timeoutSignal: AbortSignal, + appAbortSignal?: AbortSignal + ): Error { + if (timeoutSignal.aborted || get(e, 'body.message') === 'Request timed out') { + // Handle a client or a server side timeout + const err = new SearchTimeoutError(e, this.getTimeoutMode()); + + // Show the timeout error here, so that it's shown regardless of how an application chooses to handle errors. + this.showTimeoutError(err); + return err; + } else if (appAbortSignal?.aborted) { + // In the case an application initiated abort, throw the existing AbortError. + return e; + } else if (isPainlessError(e)) { + return new PainlessError(e, request); + } else { + return e; + } + } + /** * @internal */ protected runSearch( - request: IEsSearchRequest, + request: IKibanaSearchRequest, signal: AbortSignal, strategy?: string ): Observable { @@ -105,41 +126,6 @@ export class SearchInterceptor { ); } - /** - * Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort - * either when `cancelPending` is called, when the request times out, or when the original - * `AbortSignal` is aborted. Updates `pendingCount$` when the request is started/finalized. - */ - public search( - request: IEsSearchRequest, - options?: ISearchOptions - ): Observable { - // Defer the following logic until `subscribe` is actually called - return defer(() => { - if (options?.abortSignal?.aborted) { - return throwError(new AbortError()); - } - - const { combinedSignal, cleanup } = this.setupAbortSignal({ - abortSignal: options?.abortSignal, - }); - this.pendingCount$.next(this.pendingCount$.getValue() + 1); - - return this.runSearch(request, combinedSignal, options?.strategy).pipe( - catchError((e: any) => { - if (e.body?.attributes?.error === 'Request timed out') { - this.showTimeoutError(e); - } - return throwError(e); - }), - finalize(() => { - this.pendingCount$.next(this.pendingCount$.getValue() - 1); - cleanup(); - }) - ); - }); - } - /** * @internal */ @@ -156,9 +142,7 @@ export class SearchInterceptor { const timeout$ = timeout ? timer(timeout) : NEVER; const subscription = timeout$.subscribe(() => { timeoutController.abort(); - this.showTimeoutError(new AbortError()); }); - this.timeoutSubscriptions.add(subscription); // Get a combined `AbortSignal` that will be aborted whenever the first of the following occurs: // 1. The user manually aborts (via `cancelPending`) @@ -172,34 +156,95 @@ export class SearchInterceptor { const combinedSignal = getCombinedSignal(signals); const cleanup = () => { - this.timeoutSubscriptions.remove(subscription); + subscription.unsubscribe(); }; combinedSignal.addEventListener('abort', cleanup); return { combinedSignal, + timeoutSignal, cleanup, }; } - // Right now we are debouncing but we will hook this up with background sessions to show only one - // error notification per session. - protected showTimeoutError = debounce( - (e: Error) => { - this.deps.toasts.addError(e, { + /** + * Right now we are throttling but we will hook this up with background sessions to show only one + * error notification per session. + * @internal + */ + private showTimeoutError = debounce( + (e: SearchTimeoutError) => { + this.deps.toasts.addDanger({ title: 'Timed out', - toastMessage: i18n.translate('data.search.upgradeLicense', { - defaultMessage: - 'One or more queries timed out. With our free Basic tier, your queries never time out.', - }), + text: toMountPoint(e.getErrorMessage(this.application)), }); }, - 60000, - { - leading: true, - } + 30000, + { leading: true, trailing: false } ); + + /** + * Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort + * either when `cancelPending` is called, when the request times out, or when the original + * `AbortSignal` is aborted. Updates `pendingCount$` when the request is started/finalized. + * + * @param request + * @options + * @returns `Observalbe` emitting the search response or an error. + */ + public search( + request: IKibanaSearchRequest, + options?: ISearchOptions + ): Observable { + // Defer the following logic until `subscribe` is actually called + return defer(() => { + if (options?.abortSignal?.aborted) { + return throwError(new AbortError()); + } + + const { timeoutSignal, combinedSignal, cleanup } = this.setupAbortSignal({ + abortSignal: options?.abortSignal, + }); + this.pendingCount$.next(this.pendingCount$.getValue() + 1); + + return this.runSearch(request, combinedSignal, options?.strategy).pipe( + catchError((e: any) => { + return throwError( + this.handleSearchError(e, request, timeoutSignal, options?.abortSignal) + ); + }), + finalize(() => { + this.pendingCount$.next(this.pendingCount$.getValue() - 1); + cleanup(); + }) + ); + }); + } + + /* + * + */ + public showError(e: Error) { + if (e instanceof AbortError) return; + + if (e instanceof SearchTimeoutError) { + // The SearchTimeoutError is shown by the interceptor in getSearchError (regardless of how the app chooses to handle errors) + return; + } + + if (e instanceof PainlessError) { + this.deps.toasts.addDanger({ + title: 'Search Error', + text: toMountPoint(e.getErrorMessage(this.application)), + }); + return; + } + + this.deps.toasts.addError(e, { + title: 'Search Error', + }); + } } export type ISearchInterceptor = PublicMethodsOf; diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index d8937ed30e401..173baba5cab6f 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -111,6 +111,9 @@ export class SearchService implements Plugin { return { aggs: this.aggsService.start({ fieldFormats, uiSettings }), search, + showError: (e: Error) => { + this.searchInterceptor.showError(e); + }, searchSource: { /** * creates searchsource based on serialized search source fields diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index 6ae5d83499aa6..a133a8cd4be93 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -73,6 +73,8 @@ export interface ISearchStart { * {@link ISearchGeneric} */ search: ISearchGeneric; + + showError: (e: Error) => void; /** * high level search * {@link ISearchStartSearchSource} diff --git a/src/plugins/data/public/test_utils.ts b/src/plugins/data/public/test_utils.ts index f04b20e96fa1d..03dc572d0bc80 100644 --- a/src/plugins/data/public/test_utils.ts +++ b/src/plugins/data/public/test_utils.ts @@ -18,3 +18,4 @@ */ export { getFieldFormatsRegistry } from './field_formats/field_formats_registry.stub'; +export { getStubIndexPattern, StubIndexPattern } from './index_patterns/index_pattern.stub'; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 9cf7234c4a9ff..eefc373559afb 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -624,7 +624,7 @@ export type IMetricAggType = MetricAggType; export class IndexPattern implements IIndexPattern { // Warning: (ae-forgotten-export) The symbol "IndexPatternDeps" needs to be exported by the entry point index.d.ts constructor({ spec, savedObjectsClient, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps); - addScriptedField(name: string, script: string, fieldType?: string, lang?: string): Promise; + addScriptedField(name: string, script: string, fieldType?: string): Promise; // (undocumented) fieldFormatMap: Record; // Warning: (ae-forgotten-export) The symbol "IIndexPatternFieldList" needs to be exported by the entry point index.d.ts @@ -701,8 +701,6 @@ export class IndexPattern implements IIndexPattern { // (undocumented) isTimeBased(): boolean; // (undocumented) - isTimeBasedWildcard(): boolean; - // (undocumented) isTimeNanosBased(): boolean; // (undocumented) metaFields: string[]; diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 7871cc4b16464..a396033e5dedb 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -27,6 +27,12 @@ import { i18n } from '@kbn/i18n'; import { getState, splitState } from './discover_state'; import { RequestAdapter } from '../../../../inspector/public'; +import { + esFilters, + indexPatterns as indexPatternsUtils, + connectToQueryState, + syncQueryStateWithUrl, +} from '../../../../data/public'; import { SavedObjectSaveModal, showSaveModal } from '../../../../saved_objects/public'; import { getSortArray, getSortForSearchSource } from './doc_table'; import { createFixedScroll } from './directives/fixed_scroll'; @@ -34,7 +40,6 @@ import * as columnActions from './doc_table/actions/columns'; import indexTemplateLegacy from './discover_legacy.html'; import { showOpenSearchPanel } from '../components/top_nav/show_open_search_panel'; import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util'; -import { getPainlessError } from './get_painless_error'; import { discoverResponseHandler } from './response_handler'; import { getRequestInspectorStats, @@ -65,12 +70,7 @@ const { import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../helpers/breadcrumbs'; import { validateTimeRange } from '../helpers/validate_time_range'; -import { - esFilters, - indexPatterns as indexPatternsUtils, - connectToQueryState, - syncQueryStateWithUrl, -} from '../../../../data/public'; + import { getIndexPatternId } from '../helpers/get_index_pattern_id'; import { addFatalError } from '../../../../kibana_legacy/public'; import { @@ -786,18 +786,10 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise // If the request was aborted then no need to surface this error in the UI if (error instanceof Error && error.name === 'AbortError') return; - const fetchError = getPainlessError(error); + $scope.fetchStatus = fetchStatuses.NO_RESULTS; + $scope.rows = []; - if (fetchError) { - $scope.fetchError = fetchError; - } else { - toastNotifications.addError(error, { - title: i18n.translate('discover.errorLoadingData', { - defaultMessage: 'Error loading data', - }), - toastMessage: error.shortMessage || error.body?.message, - }); - } + data.search.showError(error); }); }; diff --git a/src/plugins/discover/public/application/components/discover_legacy.tsx b/src/plugins/discover/public/application/components/discover_legacy.tsx index 9c3d833d73b23..de1faaf9fc19d 100644 --- a/src/plugins/discover/public/application/components/discover_legacy.tsx +++ b/src/plugins/discover/public/application/components/discover_legacy.tsx @@ -31,7 +31,6 @@ import { DiscoverNoResults } from '../angular/directives/no_results'; import { DiscoverUninitialized } from '../angular/directives/uninitialized'; import { DiscoverHistogram } from '../angular/directives/histogram'; import { LoadingSpinner } from './loading_spinner/loading_spinner'; -import { DiscoverFetchError, FetchError } from './fetch_error/fetch_error'; import { DocTableLegacy } from '../angular/doc_table/create_doc_table_react'; import { SkipBottomButton } from './skip_bottom_button'; import { @@ -54,7 +53,6 @@ export interface DiscoverLegacyProps { addColumn: (column: string) => void; fetch: () => void; fetchCounter: number; - fetchError: FetchError; fieldCounts: Record; histogramData: Chart; hits: number; @@ -95,7 +93,6 @@ export function DiscoverLegacy({ addColumn, fetch, fetchCounter, - fetchError, fieldCounts, histogramData, hits, @@ -216,8 +213,7 @@ export function DiscoverLegacy({ {resultState === 'uninitialized' && } {/* @TODO: Solved in the Angular way to satisfy functional test - should be improved*/} - {fetchError && } -
+
diff --git a/src/plugins/discover/public/application/components/fetch_error/fetch_error.scss b/src/plugins/discover/public/application/components/fetch_error/fetch_error.scss deleted file mode 100644 index a587b2897e3a0..0000000000000 --- a/src/plugins/discover/public/application/components/fetch_error/fetch_error.scss +++ /dev/null @@ -1,3 +0,0 @@ -.discoverFetchError { - max-width: 1000px; -} diff --git a/src/plugins/discover/public/application/components/fetch_error/fetch_error.tsx b/src/plugins/discover/public/application/components/fetch_error/fetch_error.tsx deleted file mode 100644 index dc8f1238eac6f..0000000000000 --- a/src/plugins/discover/public/application/components/fetch_error/fetch_error.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import './fetch_error.scss'; -import React, { Fragment } from 'react'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import { EuiFlexGroup, EuiFlexItem, EuiCallOut, EuiCodeBlock, EuiSpacer } from '@elastic/eui'; -import { getServices } from '../../../kibana_services'; - -export interface FetchError { - lang: string; - script: string; - message: string; - error: string; -} - -interface Props { - fetchError: FetchError; -} - -export const DiscoverFetchError = ({ fetchError }: Props) => { - if (!fetchError) { - return null; - } - - let body; - - if (fetchError.lang === 'painless') { - const { chrome } = getServices(); - const mangagementUrlObj = chrome.navLinks.get('kibana:stack_management'); - const managementUrl = mangagementUrlObj ? mangagementUrlObj.url : ''; - const url = `${managementUrl}/kibana/indexPatterns`; - - body = ( -

- - ), - managementLink: ( - - - - ), - }} - /> -

- ); - } - - return ( - - - - - - - - {body} - - {fetchError.error} - - - - - - - - ); -}; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx index b03b37da40908..6211d302e7d20 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx @@ -20,13 +20,12 @@ import React from 'react'; import { findTestSubject } from '@elastic/eui/lib/test'; // @ts-ignore -import StubIndexPattern from 'test_utils/stub_index_pattern'; -// @ts-ignore import stubbedLogstashFields from 'fixtures/logstash_fields'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { DiscoverField } from './discover_field'; import { coreMock } from '../../../../../../core/public/mocks'; import { IndexPatternField } from '../../../../../data/public'; +import { getStubIndexPattern } from '../../../../../data/public/test_utils'; jest.mock('../../../kibana_services', () => ({ getServices: () => ({ @@ -53,12 +52,12 @@ jest.mock('../../../kibana_services', () => ({ })); function getComponent(selected = false, showDetails = false, useShortDots = false) { - const indexPattern = new StubIndexPattern( + const indexPattern = getStubIndexPattern( 'logstash-*', (cfg: any) => cfg, 'time', stubbedLogstashFields(), - coreMock.createStart() + coreMock.createSetup() ); const field = new IndexPatternField( diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx index 9572f35641d69..6177b60a0a7ad 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx @@ -21,8 +21,6 @@ import _ from 'lodash'; import { ReactWrapper } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; // @ts-ignore -import StubIndexPattern from 'test_utils/stub_index_pattern'; -// @ts-ignore import realHits from 'fixtures/real_hits.js'; // @ts-ignore import stubbedLogstashFields from 'fixtures/logstash_fields'; @@ -31,6 +29,7 @@ import React from 'react'; import { DiscoverSidebar, DiscoverSidebarProps } from './discover_sidebar'; import { coreMock } from '../../../../../../core/public/mocks'; import { IndexPatternAttributes } from '../../../../../data/common'; +import { getStubIndexPattern } from '../../../../../data/public/test_utils'; import { SavedObject } from '../../../../../../core/types'; jest.mock('../../../kibana_services', () => ({ @@ -65,14 +64,15 @@ jest.mock('./lib/get_index_pattern_field_list', () => ({ })); function getCompProps() { - const indexPattern = new StubIndexPattern( + const indexPattern = getStubIndexPattern( 'logstash-*', (cfg: any) => cfg, 'time', stubbedLogstashFields(), - coreMock.createStart() + coreMock.createSetup() ); + // @ts-expect-error _.each() is passing additional args to flattenHit const hits = _.each(_.cloneDeep(realHits), indexPattern.flattenHit) as Array< Record >; diff --git a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts b/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts index dad208c815675..94c76f2d5f012 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts @@ -21,11 +21,10 @@ import _ from 'lodash'; // @ts-ignore import realHits from 'fixtures/real_hits.js'; // @ts-ignore -import StubIndexPattern from 'test_utils/stub_index_pattern'; -// @ts-ignore import stubbedLogstashFields from 'fixtures/logstash_fields'; import { coreMock } from '../../../../../../../core/public/mocks'; import { IndexPattern } from '../../../../../../data/public'; +import { getStubIndexPattern } from '../../../../../../data/public/test_utils'; // @ts-ignore import { fieldCalculator } from './field_calculator'; @@ -33,12 +32,12 @@ let indexPattern: IndexPattern; describe('fieldCalculator', function () { beforeEach(function () { - indexPattern = new StubIndexPattern( + indexPattern = getStubIndexPattern( 'logstash-*', (cfg: any) => cfg, 'time', stubbedLogstashFields(), - coreMock.createStart() + coreMock.createSetup() ); }); it('should have a _countMissing that counts nulls & undefineds in an array', function () { diff --git a/src/plugins/expressions/common/expression_types/specs/error.ts b/src/plugins/expressions/common/expression_types/specs/error.ts index 35554954d0828..c95a019f4e8d2 100644 --- a/src/plugins/expressions/common/expression_types/specs/error.ts +++ b/src/plugins/expressions/common/expression_types/specs/error.ts @@ -30,6 +30,7 @@ export type ExpressionValueError = ExpressionValueBoxed< message: string; name?: string; stack?: string; + original?: Error; }; info?: unknown; } diff --git a/src/plugins/expressions/common/util/create_error.ts b/src/plugins/expressions/common/util/create_error.ts index 876e7dfec799c..9bdab74efd6f9 100644 --- a/src/plugins/expressions/common/util/create_error.ts +++ b/src/plugins/expressions/common/util/create_error.ts @@ -21,7 +21,7 @@ import { ExpressionValueError } from '../../common'; type ErrorLike = Partial>; -export const createError = (err: string | ErrorLike): ExpressionValueError => ({ +export const createError = (err: string | Error | ErrorLike): ExpressionValueError => ({ type: 'error', error: { stack: @@ -32,5 +32,6 @@ export const createError = (err: string | ErrorLike): ExpressionValueError => ({ : undefined, message: typeof err === 'string' ? err : String(err.message), name: typeof err === 'object' ? err.name || 'Error' : 'Error', + original: err instanceof Error ? err : undefined, }, }); diff --git a/src/plugins/home/public/application/components/app_navigation_handler.ts b/src/plugins/home/public/application/components/app_navigation_handler.ts index 61d85c033b544..91407ffcaf226 100644 --- a/src/plugins/home/public/application/components/app_navigation_handler.ts +++ b/src/plugins/home/public/application/components/app_navigation_handler.ts @@ -24,12 +24,6 @@ export const createAppNavigationHandler = (targetUrl: string) => (event: MouseEv if (event.altKey || event.metaKey || event.ctrlKey) { return; } - if (targetUrl.startsWith('/app/')) { - const [, appId, path] = /\/app\/(.*?)((\/|\?|#|$).*)/.exec(targetUrl) || []; - if (!appId) { - return; - } - event.preventDefault(); - getServices().application.navigateToApp(appId, { path }); - } + event.preventDefault(); + getServices().application.navigateToUrl(targetUrl); }; diff --git a/src/plugins/home/server/services/sample_data/usage/collector.ts b/src/plugins/home/server/services/sample_data/usage/collector.ts index d819d67a8d432..1cece375ce59b 100644 --- a/src/plugins/home/server/services/sample_data/usage/collector.ts +++ b/src/plugins/home/server/services/sample_data/usage/collector.ts @@ -38,12 +38,12 @@ export async function makeSampleDataUsageCollector( fetch: fetchProvider(index), isReady: () => true, schema: { - installed: { type: 'keyword' }, + installed: { type: 'array', items: { type: 'keyword' } }, last_install_date: { type: 'date' }, last_install_set: { type: 'keyword' }, last_uninstall_date: { type: 'date' }, last_uninstall_set: { type: 'keyword' }, - uninstalled: { type: 'keyword' }, + uninstalled: { type: 'array', items: { type: 'keyword' } }, }, }); diff --git a/src/plugins/maps_legacy/public/components/legacy_map_deprecation_message.tsx b/src/plugins/maps_legacy/public/components/legacy_map_deprecation_message.tsx new file mode 100644 index 0000000000000..3fae842663fdd --- /dev/null +++ b/src/plugins/maps_legacy/public/components/legacy_map_deprecation_message.tsx @@ -0,0 +1,79 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiButton, EuiCallOut, EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +interface Props { + isMapsAvailable: boolean; + onClick: (e: React.MouseEvent) => Promise; + visualizationLabel: string; +} + +export function LegacyMapDeprecationMessage(props: Props) { + const getMapsMessage = !props.isMapsAvailable ? ( + + default distribution + + ), + }} + /> + ) : null; + + const button = props.isMapsAvailable ? ( +
+ + + +
+ ) : null; + + return ( + +

+ +

+ {button} +
+ ); +} diff --git a/src/plugins/maps_legacy/public/index.ts b/src/plugins/maps_legacy/public/index.ts index d31f23f4bc4a6..fe5338b890ec8 100644 --- a/src/plugins/maps_legacy/public/index.ts +++ b/src/plugins/maps_legacy/public/index.ts @@ -63,6 +63,7 @@ export * from './common/types'; export { ORIGIN } from './common/constants/origin'; export { WmsOptions } from './components/wms_options'; +export { LegacyMapDeprecationMessage } from './components/legacy_map_deprecation_message'; export { lazyLoadMapsLegacyModules } from './lazy_load_bundle'; diff --git a/src/plugins/region_map/kibana.json b/src/plugins/region_map/kibana.json index bd5517d2a5bf7..e679baf6d6f06 100644 --- a/src/plugins/region_map/kibana.json +++ b/src/plugins/region_map/kibana.json @@ -10,7 +10,8 @@ "expressions", "mapsLegacy", "kibanaLegacy", - "data" + "data", + "share" ], "requiredBundles": [ "kibanaUtils", diff --git a/src/plugins/region_map/public/get_deprecation_message.tsx b/src/plugins/region_map/public/get_deprecation_message.tsx new file mode 100644 index 0000000000000..ea5cdf42c3111 --- /dev/null +++ b/src/plugins/region_map/public/get_deprecation_message.tsx @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { UrlGeneratorContract } from 'src/plugins/share/public'; +import { getCoreService, getQueryService, getShareService } from './kibana_services'; +import { Vis } from '../../visualizations/public'; +import { LegacyMapDeprecationMessage } from '../../maps_legacy/public'; + +function getEmsLayerId(id: string | number, layerId: string) { + if (typeof id === 'string') { + return id; + } + + // Region maps from 6.x will have numerical EMS id refering to S3 bucket id. + // In this case, use layerId with contains the EMS layer name. + const split = layerId.split('.'); + return split.length === 2 ? split[1] : undefined; +} + +export function getDeprecationMessage(vis: Vis) { + let mapsRegionMapUrlGenerator: + | UrlGeneratorContract<'MAPS_APP_REGION_MAP_URL_GENERATOR'> + | undefined; + try { + mapsRegionMapUrlGenerator = getShareService().urlGenerators.getUrlGenerator( + 'MAPS_APP_REGION_MAP_URL_GENERATOR' + ); + } catch (error) { + // ignore error thrown when url generator is not available + } + + const title = i18n.translate('regionMap.mapVis.regionMapTitle', { defaultMessage: 'Region Map' }); + + async function onClick(e: React.MouseEvent) { + e.preventDefault(); + + const query = getQueryService(); + const createUrlParams: { [key: string]: any } = { + label: vis.title ? vis.title : title, + emsLayerId: vis.params.selectedLayer.isEMS + ? getEmsLayerId(vis.params.selectedLayer.id, vis.params.selectedLayer.layerId) + : undefined, + leftFieldName: vis.params.selectedLayer.isEMS ? vis.params.selectedJoinField.name : undefined, + colorSchema: vis.params.colorSchema, + indexPatternId: vis.data.indexPattern?.id, + indexPatternTitle: vis.data.indexPattern?.title, + metricAgg: 'count', + filters: query.filterManager.getFilters(), + query: query.queryString.getQuery(), + timeRange: query.timefilter.timefilter.getTime(), + }; + + const bucketAggs = vis.data?.aggs?.byType('buckets'); + if (bucketAggs?.length && bucketAggs[0].type.dslName === 'terms') { + createUrlParams.termsFieldName = bucketAggs[0].getField()?.name; + } + + const metricAggs = vis.data?.aggs?.byType('metrics'); + if (metricAggs?.length) { + createUrlParams.metricAgg = metricAggs[0].type.dslName; + createUrlParams.metricFieldName = metricAggs[0].getField()?.name; + } + + const url = await mapsRegionMapUrlGenerator!.createUrl(createUrlParams); + getCoreService().application.navigateToUrl(url); + } + + return ( + + ); +} diff --git a/src/plugins/region_map/public/kibana_services.ts b/src/plugins/region_map/public/kibana_services.ts index 8367325c7415b..7edbf2da36fc7 100644 --- a/src/plugins/region_map/public/kibana_services.ts +++ b/src/plugins/region_map/public/kibana_services.ts @@ -17,10 +17,14 @@ * under the License. */ +import { CoreStart } from 'kibana/public'; import { NotificationsStart } from 'kibana/public'; import { createGetterSetter } from '../../kibana_utils/public'; import { DataPublicPluginStart } from '../../data/public'; import { KibanaLegacyStart } from '../../kibana_legacy/public'; +import { SharePluginStart } from '../../share/public'; + +export const [getCoreService, setCoreService] = createGetterSetter('Core'); export const [getFormatService, setFormatService] = createGetterSetter< DataPublicPluginStart['fieldFormats'] @@ -30,6 +34,12 @@ export const [getNotifications, setNotifications] = createGetterSetter('Query'); + +export const [getShareService, setShareService] = createGetterSetter('Share'); + export const [getKibanaLegacy, setKibanaLegacy] = createGetterSetter( 'KibanaLegacy' ); diff --git a/src/plugins/region_map/public/plugin.ts b/src/plugins/region_map/public/plugin.ts index c641c16a8112b..e9978803ad5e2 100644 --- a/src/plugins/region_map/public/plugin.ts +++ b/src/plugins/region_map/public/plugin.ts @@ -31,11 +31,19 @@ import { createRegionMapFn } from './region_map_fn'; // @ts-ignore import { createRegionMapTypeDefinition } from './region_map_type'; import { IServiceSettings, MapsLegacyPluginSetup } from '../../maps_legacy/public'; -import { setFormatService, setNotifications, setKibanaLegacy } from './kibana_services'; +import { + setCoreService, + setFormatService, + setNotifications, + setKibanaLegacy, + setQueryService, + setShareService, +} from './kibana_services'; import { DataPublicPluginStart } from '../../data/public'; import { RegionMapsConfigType } from './index'; import { MapsLegacyConfig } from '../../maps_legacy/config'; import { KibanaLegacyStart } from '../../kibana_legacy/public'; +import { SharePluginStart } from '../../share/public'; /** @private */ interface RegionMapVisualizationDependencies { @@ -57,6 +65,7 @@ export interface RegionMapPluginStartDependencies { data: DataPublicPluginStart; notifications: NotificationsStart; kibanaLegacy: KibanaLegacyStart; + share: SharePluginStart; } /** @internal */ @@ -108,10 +117,13 @@ export class RegionMapPlugin implements Plugin { type: 'dashboard', } as SimpleSavedObject); - const indexPattern = new StubIndexPattern( + const indexPattern = getStubIndexPattern( 'my-index', getConfig, null, [], coreMock.createSetup() ); - indexPattern.title = indexPattern.id; + indexPattern.title = indexPattern.id!; savedObject.searchSource!.setField('index', indexPattern); return savedObject.save(saveOptionsMock).then(() => { const args = (savedObjectsClientStub.create as jest.Mock).mock.calls[0]; @@ -335,7 +334,7 @@ describe('Saved Object', () => { type: 'dashboard', } as SimpleSavedObject); - const indexPattern = new StubIndexPattern( + const indexPattern = getStubIndexPattern( 'non-existant-index', getConfig, null, @@ -662,14 +661,14 @@ describe('Saved Object', () => { const savedObject = new SavedObjectClass(config); savedObject.hydrateIndexPattern = jest.fn().mockImplementation(() => { - const indexPattern = new StubIndexPattern( + const indexPattern = getStubIndexPattern( indexPatternId, getConfig, null, [], coreMock.createSetup() ); - indexPattern.title = indexPattern.id; + indexPattern.title = indexPattern.id!; savedObject.searchSource!.setField('index', indexPattern); return Bluebird.resolve(indexPattern); }); diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 3ee0c181203aa..6531262b6f1da 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -29,7 +29,10 @@ "sample-data": { "properties": { "installed": { - "type": "keyword" + "type": "array", + "items": { + "type": "keyword" + } }, "last_install_date": { "type": "date" @@ -44,7 +47,10 @@ "type": "keyword" }, "uninstalled": { - "type": "keyword" + "type": "array", + "items": { + "type": "keyword" + } } } }, diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap index ab29656c557c2..bed1bbeabb044 100644 --- a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap +++ b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap @@ -49,7 +49,7 @@ exports[`TelemetryManagementSectionComponent renders as expected 1`] = ` - `; -exports[`TelemetryManagementSectionComponent renders null because query does not match the SEARCH_TERMS 1`] = ` - -`; - exports[`TelemetryManagementSectionComponent test the wrapper (for coverage purposes) 1`] = `null`; diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx index c13f639f31447..0e2855f055540 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx @@ -21,6 +21,7 @@ import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import TelemetryManagementSection from './telemetry_management_section'; import { TelemetryService } from '../../../telemetry/public/services'; import { coreMock } from '../../../../core/public/mocks'; +import { render } from '@testing-library/react'; describe('TelemetryManagementSectionComponent', () => { const coreStart = coreMock.createStart(); @@ -73,19 +74,31 @@ describe('TelemetryManagementSectionComponent', () => { http: coreSetup.http, }); - const component = mountWithIntl( - + const component = render( + Fallback}> + + ); + try { - expect( - component.setProps({ ...component.props(), query: { text: 'asssdasdsad' } }) - ).toMatchSnapshot(); + component.rerender( + Fallback}> + + + ); expect(onQueryMatchChange).toHaveBeenCalledWith(false); expect(onQueryMatchChange).toHaveBeenCalledTimes(1); } finally { diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx index db104e9c7baab..9ae0a3d12fbb5 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx @@ -34,7 +34,7 @@ import { i18n } from '@kbn/i18n'; import { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; import { PRIVACY_STATEMENT_URL } from '../../../telemetry/common/constants'; import { OptInExampleFlyout } from './opt_in_example_flyout'; -import { Field } from '../../../advanced_settings/public'; +import { LazyField } from '../../../advanced_settings/public'; import { ToastsStart } from '../../../../core/public'; type TelemetryService = TelemetryPluginSetup['telemetryService']; @@ -119,7 +119,7 @@ export class TelemetryManagementSection extends Component { {this.maybeGetAppliesSettingMessage()} - | undefined; + try { + mapsTileMapUrlGenerator = getShareService().urlGenerators.getUrlGenerator( + 'MAPS_APP_TILE_MAP_URL_GENERATOR' + ); + } catch (error) { + // ignore error thrown when url generator is not available + } + + const title = i18n.translate('tileMap.vis.mapTitle', { + defaultMessage: 'Coordinate Map', + }); + + async function onClick(e: React.MouseEvent) { + e.preventDefault(); + + const query = getQueryService(); + const createUrlParams: { [key: string]: any } = { + label: vis.title ? vis.title : title, + mapType: vis.params.mapType, + colorSchema: vis.params.colorSchema, + indexPatternId: vis.data.indexPattern?.id, + metricAgg: 'count', + filters: query.filterManager.getFilters(), + query: query.queryString.getQuery(), + timeRange: query.timefilter.timefilter.getTime(), + }; + + const bucketAggs = vis.data?.aggs?.byType('buckets'); + if (bucketAggs?.length && bucketAggs[0].type.dslName === 'geohash_grid') { + createUrlParams.geoFieldName = bucketAggs[0].getField()?.name; + } else if (vis.data.indexPattern) { + // attempt to default to first geo point field when geohash is not configured yet + const geoField = vis.data.indexPattern.fields.find((field) => { + return ( + !indexPatterns.isNestedField(field) && field.aggregatable && field.type === 'geo_point' + ); + }); + if (geoField) { + createUrlParams.geoFieldName = geoField.name; + } + } + + const metricAggs = vis.data?.aggs?.byType('metrics'); + if (metricAggs?.length) { + createUrlParams.metricAgg = metricAggs[0].type.dslName; + createUrlParams.metricFieldName = metricAggs[0].getField()?.name; + } + + const url = await mapsTileMapUrlGenerator!.createUrl(createUrlParams); + getCoreService().application.navigateToUrl(url); + } + + return ( + + ); +} diff --git a/src/plugins/tile_map/public/plugin.ts b/src/plugins/tile_map/public/plugin.ts index 07add6901fb49..dfcafafbe47f7 100644 --- a/src/plugins/tile_map/public/plugin.ts +++ b/src/plugins/tile_map/public/plugin.ts @@ -34,8 +34,15 @@ import { createTileMapFn } from './tile_map_fn'; import { createTileMapTypeDefinition } from './tile_map_type'; import { IServiceSettings, MapsLegacyPluginSetup } from '../../maps_legacy/public'; import { DataPublicPluginStart } from '../../data/public'; -import { setFormatService, setQueryService, setKibanaLegacy } from './services'; +import { + setCoreService, + setFormatService, + setQueryService, + setKibanaLegacy, + setShareService, +} from './services'; import { KibanaLegacyStart } from '../../kibana_legacy/public'; +import { SharePluginStart } from '../../share/public'; export interface TileMapConfigType { tilemap: any; @@ -61,6 +68,7 @@ export interface TileMapPluginSetupDependencies { export interface TileMapPluginStartDependencies { data: DataPublicPluginStart; kibanaLegacy: KibanaLegacyStart; + share: SharePluginStart; } export interface TileMapPluginSetup { @@ -100,10 +108,12 @@ export class TileMapPlugin implements Plugin('Core'); export const [getFormatService, setFormatService] = createGetterSetter< DataPublicPluginStart['fieldFormats'] @@ -29,6 +33,8 @@ export const [getQueryService, setQueryService] = createGetterSetter< DataPublicPluginStart['query'] >('Query'); +export const [getShareService, setShareService] = createGetterSetter('Share'); + export const [getKibanaLegacy, setKibanaLegacy] = createGetterSetter( 'KibanaLegacy' ); diff --git a/src/plugins/tile_map/public/tile_map_type.js b/src/plugins/tile_map/public/tile_map_type.js index 2b23f345f012e..7073958a1b318 100644 --- a/src/plugins/tile_map/public/tile_map_type.js +++ b/src/plugins/tile_map/public/tile_map_type.js @@ -25,6 +25,7 @@ import { createTileMapVisualization } from './tile_map_visualization'; import { TileMapOptions } from './components/tile_map_options'; import { supportsCssFilters } from './css_filters'; import { truncatedColorSchemas } from '../../charts/public'; +import { getDeprecationMessage } from './get_deprecation_message'; export function createTileMapTypeDefinition(dependencies) { const CoordinateMapsVisualization = createTileMapVisualization(dependencies); @@ -32,6 +33,8 @@ export function createTileMapTypeDefinition(dependencies) { return { name: 'tile_map', + isDeprecated: true, + getDeprecationMessage, title: i18n.translate('tileMap.vis.mapTitle', { defaultMessage: 'Coordinate Map', }), diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index d8edc5bb8d18a..9955f9fac81ca 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -140,6 +140,14 @@ The `AllowedSchemaTypes` is the list of allowed schema types for the usage field 'keyword', 'text', 'number', 'boolean', 'long', 'date', 'float' ``` +### Arrays + +If any of your properties is an array, the schema definition must follow the convention below: + +``` +{ type: 'array', items: {...mySchemaDefinitionOfTheEntriesInTheArray} } +``` + ### Example ```ts @@ -152,6 +160,8 @@ export const myCollector = makeUsageCollector({ some_obj: { total: 123, }, + some_array: ['value1', 'value2'], + some_array_of_obj: [{total: 123}], }; }, schema: { @@ -163,6 +173,18 @@ export const myCollector = makeUsageCollector({ type: 'number', }, }, + some_array: { + type: 'array', + items: { type: 'keyword' } + }, + some_array_of_obj: { + type: 'array', + items: { + total: { + type: 'number', + }, + }, + }, }, }); ``` diff --git a/src/plugins/usage_collection/server/collector/collector.test.ts b/src/plugins/usage_collection/server/collector/collector.test.ts index a3e2425c1f122..375fe4f7686c0 100644 --- a/src/plugins/usage_collection/server/collector/collector.test.ts +++ b/src/plugins/usage_collection/server/collector/collector.test.ts @@ -153,7 +153,10 @@ describe('collector', () => { isReady: () => false, fetch: () => ({ testPass: [{ name: 'a', value: 100 }] }), schema: { - testPass: { name: { type: 'keyword' }, value: { type: 'long' } }, + testPass: { + type: 'array', + items: { name: { type: 'keyword' }, value: { type: 'long' } }, + }, }, }); expect(collector).toBeDefined(); @@ -166,7 +169,10 @@ describe('collector', () => { fetch: () => ({ testPass: [{ name: 'a', value: 100 }], otherProp: 1 }), // @ts-expect-error schema: { - testPass: { name: { type: 'keyword' }, value: { type: 'long' } }, + testPass: { + type: 'array', + items: { name: { type: 'keyword' }, value: { type: 'long' } }, + }, }, }); expect(collector).toBeDefined(); @@ -185,7 +191,10 @@ describe('collector', () => { }, // @ts-expect-error schema: { - testPass: { name: { type: 'keyword' }, value: { type: 'long' } }, + testPass: { + type: 'array', + items: { name: { type: 'keyword' }, value: { type: 'long' } }, + }, }, }); expect(collector).toBeDefined(); @@ -203,7 +212,10 @@ describe('collector', () => { return { otherProp: 1 }; }, schema: { - testPass: { name: { type: 'keyword' }, value: { type: 'long' } }, + testPass: { + type: 'array', + items: { name: { type: 'keyword' }, value: { type: 'long' } }, + }, otherProp: { type: 'long' }, }, }); diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index 365e1ce201337..b0bc18a0cf0eb 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -40,7 +40,7 @@ export type RecursiveMakeSchemaFrom = U extends object export type MakeSchemaFrom = { [Key in keyof Base]: Base[Key] extends Array - ? RecursiveMakeSchemaFrom + ? { type: 'array'; items: RecursiveMakeSchemaFrom } : RecursiveMakeSchemaFrom; }; diff --git a/src/plugins/vis_type_table/public/table_vis_controller.test.ts b/src/plugins/vis_type_table/public/table_vis_controller.test.ts index 7535e98d391c6..2b4017ae0ee81 100644 --- a/src/plugins/vis_type_table/public/table_vis_controller.test.ts +++ b/src/plugins/vis_type_table/public/table_vis_controller.test.ts @@ -22,8 +22,6 @@ import 'angular-mocks'; import 'angular-sanitize'; import $ from 'jquery'; -// @ts-ignore -import StubIndexPattern from 'test_utils/stub_index_pattern'; import { getAngularModule } from './get_inner_angular'; import { initTableVisLegacyModule } from './table_vis_legacy_module'; import { getTableVisTypeDefinition } from './table_vis_type'; @@ -32,6 +30,7 @@ import { stubFields } from '../../data/public/stubs'; import { tableVisResponseHandler } from './table_vis_response_handler'; import { coreMock } from '../../../core/public/mocks'; import { IAggConfig, search } from '../../data/public'; +import { getStubIndexPattern } from '../../data/public/test_utils'; // TODO: remove linting disable import { searchServiceMock } from '../../data/public/search/mocks'; @@ -105,7 +104,7 @@ describe('Table Vis - Controller', () => { ); beforeEach(() => { - stubIndexPattern = new StubIndexPattern( + stubIndexPattern = getStubIndexPattern( 'logstash-*', (cfg: any) => cfg, 'time', diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index c14148d4a020f..2434285bd94c6 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -163,14 +163,9 @@ export class TimeseriesVisualization extends Component { const mainAxisGroupId = yAxisIdGenerator('main_group'); const seriesModel = model.series.filter((s) => !s.hidden).map((s) => cloneDeep(s)); - const firstSeries = seriesModel.find((s) => s.formatter && !s.separate_axis); const mainAxisScaleType = TimeseriesVisualization.getAxisScaleType(model); const mainAxisDomain = TimeseriesVisualization.getYAxisDomain(model); - const tickFormatter = TimeseriesVisualization.getTickFormatter( - firstSeries, - this.props.getConfig - ); const yAxis = []; let mainDomainAdded = false; @@ -203,7 +198,7 @@ export class TimeseriesVisualization extends Component { series .filter((r) => startsWith(r.id, seriesGroup.id)) .forEach((seriesDataRow) => { - seriesDataRow.tickFormatter = seriesGroupTickFormatter; + seriesDataRow.tickFormat = seriesGroupTickFormatter; seriesDataRow.groupId = groupId; seriesDataRow.yScaleType = yScaleType; seriesDataRow.hideInLegend = Boolean(seriesGroup.hide_in_legend); @@ -224,7 +219,7 @@ export class TimeseriesVisualization extends Component { }); } else if (!mainDomainAdded) { TimeseriesVisualization.addYAxis(yAxis, { - tickFormatter, + tickFormatter: series.length === 1 ? undefined : (val) => val, id: yAxisIdGenerator('main'), groupId: mainAxisGroupId, position: model.axis_position, diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.js index 300af551e5020..561c985b3b5c3 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.js @@ -42,6 +42,7 @@ export function AreaSeriesDecorator({ sortIndex, y1AccessorFormat, y0AccessorFormat, + tickFormat, }) { const id = seriesId; const groupId = seriesGroupId; @@ -67,6 +68,7 @@ export function AreaSeriesDecorator({ enableHistogramMode, useDefaultGroupDomain, sortIndex, + tickFormat, ...areaSeriesStyle, }; diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.js index 239f1d4f1838e..2d6c54de41431 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.js @@ -41,6 +41,7 @@ export function BarSeriesDecorator({ sortIndex, y1AccessorFormat, y0AccessorFormat, + tickFormat, }) { const id = seriesId; const groupId = seriesGroupId; @@ -66,6 +67,7 @@ export function BarSeriesDecorator({ enableHistogramMode, useDefaultGroupDomain, sortIndex, + tickFormat, ...barSeriesStyle, }; diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js index 5f0cc5188b1fd..1209a105af805 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js @@ -176,6 +176,7 @@ export const TimeSeries = ({ useDefaultGroupDomain, y1AccessorFormat, y0AccessorFormat, + tickFormat, }, sortIndex ) => { @@ -207,6 +208,7 @@ export const TimeSeries = ({ sortIndex={sortIndex} y1AccessorFormat={y1AccessorFormat} y0AccessorFormat={y0AccessorFormat} + tickFormat={tickFormat} /> ); } @@ -233,6 +235,7 @@ export const TimeSeries = ({ sortIndex={sortIndex} y1AccessorFormat={y1AccessorFormat} y0AccessorFormat={y0AccessorFormat} + tickFormat={tickFormat} /> ); } diff --git a/src/plugins/visualizations/public/types.ts b/src/plugins/visualizations/public/types.ts index 897a8c1e32319..68ab3561d375c 100644 --- a/src/plugins/visualizations/public/types.ts +++ b/src/plugins/visualizations/public/types.ts @@ -73,7 +73,7 @@ export interface VisToExpressionAstParams { abortSignal?: AbortSignal; } -export type VisToExpressionAst = ( - vis: Vis, +export type VisToExpressionAst = ( + vis: Vis, params: VisToExpressionAstParams ) => ExpressionAstExpression; diff --git a/src/plugins/visualizations/public/vis_types/base_vis_type.ts b/src/plugins/visualizations/public/vis_types/base_vis_type.ts index 283286648ff16..149146bf77e73 100644 --- a/src/plugins/visualizations/public/vis_types/base_vis_type.ts +++ b/src/plugins/visualizations/public/vis_types/base_vis_type.ts @@ -18,9 +18,11 @@ */ import _ from 'lodash'; -import { VisToExpressionAst, VisualizationControllerConstructor } from '../types'; +import { ReactElement } from 'react'; +import { VisParams, VisToExpressionAst, VisualizationControllerConstructor } from '../types'; import { TriggerContextMapping } from '../../../ui_actions/public'; import { Adapters } from '../../../inspector/public'; +import { Vis } from '../vis'; interface CommonBaseVisTypeOptions { name: string; @@ -41,10 +43,12 @@ interface CommonBaseVisTypeOptions { setup?: unknown; useCustomNoDataScreen?: boolean; inspectorAdapters?: Adapters | (() => Adapters); + isDeprecated?: boolean; + getDeprecationMessage?: (vis: Vis) => ReactElement; } -interface ExpressionBaseVisTypeOptions extends CommonBaseVisTypeOptions { - toExpressionAst: VisToExpressionAst; +interface ExpressionBaseVisTypeOptions extends CommonBaseVisTypeOptions { + toExpressionAst: VisToExpressionAst; visualization?: undefined; } @@ -53,9 +57,11 @@ interface VisualizationBaseVisTypeOptions extends CommonBaseVisTypeOptions { visualization: VisualizationControllerConstructor | undefined; } -export type BaseVisTypeOptions = ExpressionBaseVisTypeOptions | VisualizationBaseVisTypeOptions; +export type BaseVisTypeOptions = + | ExpressionBaseVisTypeOptions + | VisualizationBaseVisTypeOptions; -export class BaseVisType { +export class BaseVisType { name: string; title: string; description: string; @@ -77,9 +83,11 @@ export class BaseVisType { setup?: unknown; useCustomNoDataScreen: boolean; inspectorAdapters?: Adapters | (() => Adapters); - toExpressionAst?: VisToExpressionAst; + toExpressionAst?: VisToExpressionAst; + isDeprecated: boolean; + getDeprecationMessage?: (vis: Vis) => ReactElement; - constructor(opts: BaseVisTypeOptions) { + constructor(opts: BaseVisTypeOptions) { if (!opts.icon && !opts.image) { throw new Error('vis_type must define its icon or image'); } @@ -115,6 +123,8 @@ export class BaseVisType { this.useCustomNoDataScreen = opts.useCustomNoDataScreen || false; this.inspectorAdapters = opts.inspectorAdapters; this.toExpressionAst = opts.toExpressionAst; + this.isDeprecated = opts.isDeprecated || false; + this.getDeprecationMessage = opts.getDeprecationMessage; } public get schemas() { diff --git a/src/plugins/visualizations/public/vis_types/types_service.ts b/src/plugins/visualizations/public/vis_types/types_service.ts index 157dbd41ce8a2..1afbd6901a195 100644 --- a/src/plugins/visualizations/public/vis_types/types_service.ts +++ b/src/plugins/visualizations/public/vis_types/types_service.ts @@ -71,7 +71,7 @@ export class TypesService { * registers a visualization type * @param config - visualization type definition */ - createBaseVisualization: (config: BaseVisTypeOptions): void => { + createBaseVisualization: (config: BaseVisTypeOptions): void => { const vis = new BaseVisType(config); registerVisualization(() => vis); }, diff --git a/src/plugins/visualize/public/application/components/visualize_editor_common.tsx b/src/plugins/visualize/public/application/components/visualize_editor_common.tsx index b811936c63b14..4321d7dd1a6ca 100644 --- a/src/plugins/visualize/public/application/components/visualize_editor_common.tsx +++ b/src/plugins/visualize/public/application/components/visualize_editor_common.tsx @@ -79,28 +79,34 @@ export const VisualizeEditorCommon = ({ /> )} {visInstance?.vis?.type?.isExperimental && } + {visInstance?.vis?.type?.isDeprecated && + visInstance?.vis?.type?.getDeprecationMessage && + visInstance.vis.type.getDeprecationMessage(visInstance?.vis)} {visInstance && (

- {'savedVis' in visInstance && visInstance.savedVis.id ? ( - - ) : ( - - )} + { + // @ts-expect-error + 'savedVis' in visInstance && visInstance.savedVis.id ? ( + + ) : ( + + ) + }

)} diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts index 31f0fc5f94479..bb4fabb189a27 100644 --- a/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts @@ -50,6 +50,8 @@ describe('getVisualizationInstance', () => { }; savedVisMock = {}; // @ts-expect-error + mockServices.data.search.showError.mockImplementation(() => {}); + // @ts-expect-error mockServices.savedVisualizations.get.mockImplementation(() => savedVisMock); // @ts-expect-error mockServices.visualizations.convertToSerializedVis.mockImplementation(() => serializedVisMock); @@ -119,6 +121,6 @@ describe('getVisualizationInstance', () => { error: 'error', }); - expect(mockServices.toastNotifications.addError).toHaveBeenCalled(); + expect(mockServices.data.search.showError).toHaveBeenCalled(); }); }); diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts index 3ffca578f8052..c5cfa5a4c639b 100644 --- a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts @@ -17,7 +17,6 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; import { SerializedVis, Vis, @@ -28,6 +27,7 @@ import { import { SearchSourceFields } from 'src/plugins/data/public'; import { SavedObject } from 'src/plugins/saved_objects/public'; import { cloneDeep } from 'lodash'; +import { ExpressionValueError } from 'src/plugins/expressions/public'; import { createSavedSearchesLoader } from '../../../../discover/public'; import { VisualizeServices } from '../types'; @@ -35,14 +35,7 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async ( vis: Vis, visualizeServices: VisualizeServices ) => { - const { - chrome, - data, - overlays, - createVisEmbeddableFromObject, - savedObjects, - toastNotifications, - } = visualizeServices; + const { chrome, data, overlays, createVisEmbeddableFromObject, savedObjects } = visualizeServices; const embeddableHandler = (await createVisEmbeddableFromObject(vis, { timeRange: data.query.timefilter.timefilter.getTime(), filters: data.query.filterManager.getFilters(), @@ -51,11 +44,9 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async ( embeddableHandler.getOutput$().subscribe((output) => { if (output.error) { - toastNotifications.addError(output.error, { - title: i18n.translate('visualize.error.title', { - defaultMessage: 'Visualization error', - }), - }); + data.search.showError( + ((output.error as unknown) as ExpressionValueError['error']).original || output.error + ); } }); diff --git a/src/test_utils/public/stub_index_pattern.js b/src/test_utils/public/stub_index_pattern.js deleted file mode 100644 index 6130a7f9f9e87..0000000000000 --- a/src/test_utils/public/stub_index_pattern.js +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import sinon from 'sinon'; -// TODO: We should not be importing from the data plugin directly here; this is only necessary -// because it is one of the few places that we need to access the IndexPattern class itself, rather -// than just the type. Doing this as a temporary measure; it will be left behind when migrating to NP. - -import { getFieldFormatsRegistry } from '../../plugins/data/public/test_utils'; -import { IndexPattern, indexPatterns, KBN_FIELD_TYPES, fieldList } from '../../plugins/data/public'; - -import { setFieldFormats } from '../../plugins/data/public/services'; - -setFieldFormats({ - getDefaultInstance: () => ({ - getConverterFor: () => (value) => value, - convert: (value) => JSON.stringify(value), - }), -}); - -export default function StubIndexPattern(pattern, getConfig, timeField, fields, core) { - const registeredFieldFormats = getFieldFormatsRegistry(core); - - this.id = pattern; - this.title = pattern; - this.popularizeField = sinon.stub(); - this.timeFieldName = timeField; - this.isTimeBased = () => Boolean(this.timeFieldName); - this.getConfig = getConfig; - this.getNonScriptedFields = sinon.spy(IndexPattern.prototype.getNonScriptedFields); - this.getScriptedFields = sinon.spy(IndexPattern.prototype.getScriptedFields); - this.getFieldByName = sinon.spy(IndexPattern.prototype.getFieldByName); - this.getSourceFiltering = sinon.stub(); - this.metaFields = ['_id', '_type', '_source']; - this.fieldFormatMap = {}; - - this.getComputedFields = IndexPattern.prototype.getComputedFields.bind(this); - this.flattenHit = indexPatterns.flattenHitWrapper(this, this.metaFields); - this.formatHit = indexPatterns.formatHitProvider( - this, - registeredFieldFormats.getDefaultInstance(KBN_FIELD_TYPES.STRING) - ); - this.fieldsFetcher = { apiClient: { baseUrl: '' } }; - this.formatField = this.formatHit.formatField; - this.getFormatterForField = () => ({ - convert: () => '', - }); - - this._reindexFields = function () { - this.fields = fieldList(this.fields || fields, false); - }; - - this.stubSetFieldFormat = function (fieldName, id, params) { - const FieldFormat = registeredFieldFormats.getType(id); - this.fieldFormatMap[fieldName] = new FieldFormat(params); - this._reindexFields(); - }; - - this._reindexFields(); -} diff --git a/test/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts index 44639af9da9f8..4ca6c936143df 100644 --- a/test/accessibility/apps/discover.ts +++ b/test/accessibility/apps/discover.ts @@ -21,21 +21,14 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'discover', 'header', 'share', 'timePicker']); - const retry = getService('retry'); const a11y = getService('a11y'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const inspector = getService('inspector'); - const docTable = getService('docTable'); - const filterBar = getService('filterBar'); - const TEST_COLUMN_NAMES = ['@message']; - const TEST_FILTER_COLUMN_NAMES = [ - ['extension', 'jpg'], - ['geo.src', 'IN'], - ]; - - // Failing: See https://github.com/elastic/kibana/issues/59975 - describe.skip('Discover', () => { + const testSubjects = getService('testSubjects'); + const TEST_COLUMN_NAMES = ['extension', 'geo.src']; + + describe('Discover a11y tests', () => { before(async () => { await esArchiver.load('discover'); await esArchiver.loadIfNeeded('logstash_functional'); @@ -46,105 +39,85 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); }); - it('main view', async () => { + it('Discover main page', async () => { await a11y.testAppSnapshot(); }); - it('Click save button', async () => { + it('a11y test on save button', async () => { await PageObjects.discover.clickSaveSearchButton(); await a11y.testAppSnapshot(); }); - it('Save search panel', async () => { + it('a11y test on save search panel', async () => { await PageObjects.discover.inputSavedSearchTitle('a11ySearch'); await a11y.testAppSnapshot(); }); - it('Confirm saved search', async () => { + it('a11y test on clicking on confirm save', async () => { await PageObjects.discover.clickConfirmSavedSearch(); await a11y.testAppSnapshot(); }); - it('Click on new to clear the search', async () => { + it('a11y test on click new to reload discover', async () => { await PageObjects.discover.clickNewSearchButton(); await a11y.testAppSnapshot(); }); - it('Open load saved search panel', async () => { + it('a11y test on load saved search panel', async () => { await PageObjects.discover.openLoadSavedSearchPanel(); await a11y.testAppSnapshot(); await PageObjects.discover.closeLoadSavedSearchPanel(); }); - it('Open inspector panel', async () => { + it('a11y test on inspector panel', async () => { await inspector.open(); await a11y.testAppSnapshot(); await inspector.close(); }); - it('Open add filter', async () => { - await PageObjects.discover.openAddFilterPanel(); - await a11y.testAppSnapshot(); - }); - - it('Select values for a filter', async () => { - await filterBar.addFilter('extension.raw', 'is one of', 'jpg'); - await a11y.testAppSnapshot(); - }); - - it('Load a new search from the panel', async () => { - await PageObjects.discover.clickSaveSearchButton(); - await PageObjects.discover.inputSavedSearchTitle('filterSearch'); - await PageObjects.discover.clickConfirmSavedSearch(); - await PageObjects.discover.openLoadSavedSearchPanel(); - await PageObjects.discover.loadSavedSearch('filterSearch'); - await a11y.testAppSnapshot(); - }); - - it('click share button', async () => { + it('a11y test on share panel', async () => { await PageObjects.share.clickShareTopNavButton(); await a11y.testAppSnapshot(); }); - it('Open sidebar filter', async () => { + it('a11y test on open sidenav filter', async () => { await PageObjects.discover.openSidebarFieldFilter(); await a11y.testAppSnapshot(); - }); - - it('Close sidebar filter', async () => { await PageObjects.discover.closeSidebarFieldFilter(); - await a11y.testAppSnapshot(); }); - it('Add a field from sidebar', async () => { + it('a11y test on tables with columns view', async () => { for (const columnName of TEST_COLUMN_NAMES) { - await PageObjects.discover.clickFieldListItemAdd(columnName); + await PageObjects.discover.clickFieldListItemToggle(columnName); } await a11y.testAppSnapshot(); }); - it('Add more fields from sidebar', async () => { - for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) { - await PageObjects.discover.clickFieldListItem(columnName); - await PageObjects.discover.clickFieldListPlusFilter(columnName, value); - } + it('a11y test on save queries popover', async () => { + await PageObjects.discover.clickSavedQueriesPopOver(); await a11y.testAppSnapshot(); }); - // Context view test - it('should open context view on a doc', async () => { - await retry.try(async () => { - await docTable.clickRowToggle(); - // click the open action - const rowActions = await docTable.getRowActions(); - if (!rowActions.length) { - throw new Error('row actions empty, trying again'); - } - await rowActions[0].click(); - }); + it('a11y test on save queries panel', async () => { + await PageObjects.discover.clickCurrentSavedQuery(); await a11y.testAppSnapshot(); }); - // Adding rest of the tests after https://github.com/elastic/kibana/issues/53888 is resolved + it('a11y test on toggle include filters option on saved queries panel', async () => { + await PageObjects.discover.setSaveQueryFormTitle('test'); + await PageObjects.discover.toggleIncludeFilters(); + await a11y.testAppSnapshot(); + await PageObjects.discover.saveCurrentSavedQuery(); + }); + + // issue - https://github.com/elastic/kibana/issues/78488 + it.skip('a11y test on saved queries list panel', async () => { + await PageObjects.discover.clickSavedQueriesPopOver(); + await testSubjects.moveMouseTo( + 'saved-query-list-item load-saved-query-test-button saved-query-list-item-selected saved-query-list-item-selected' + ); + await testSubjects.find('delete-saved-query-test-button'); + await a11y.testAppSnapshot(); + }); }); } diff --git a/test/functional/apps/discover/_errors.ts b/test/functional/apps/discover/_errors.ts index 9520d652a65d5..7f1552b90668b 100644 --- a/test/functional/apps/discover/_errors.ts +++ b/test/functional/apps/discover/_errors.ts @@ -22,7 +22,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const testSubjects = getService('testSubjects'); + const toasts = getService('toasts'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); describe('errors', function describeIndexTests() { @@ -39,8 +39,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('invalid scripted field error', () => { it('is rendered', async () => { - const isFetchErrorVisible = await testSubjects.exists('discoverFetchError'); - expect(isFetchErrorVisible).to.be(true); + const toast = await toasts.getToastElement(1); + const painlessStackTrace = await toast.findByTestSubject('painlessStackTrace'); + expect(painlessStackTrace).not.to.be(undefined); }); }); }); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 7a99509257bf7..e522f41952a49 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -119,8 +119,7 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider public async loadSavedSearch(searchName: string) { await this.openLoadSavedSearchPanel(); - const searchLink = await find.byButtonText(searchName); - await searchLink.click(); + await testSubjects.click(`savedObjectTitle${searchName.split(' ').join('-')}`); await header.waitUntilLoadingHasFinished(); } @@ -387,6 +386,42 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider return await this.isDiscoverAppOnScreen(); }); } + + public async showAllFilterActions() { + await testSubjects.click('showFilterActions'); + } + + public async clickSavedQueriesPopOver() { + await testSubjects.click('saved-query-management-popover-button'); + } + + public async clickCurrentSavedQuery() { + await testSubjects.click('saved-query-management-save-button'); + } + + public async setSaveQueryFormTitle(savedQueryName: string) { + await testSubjects.setValue('saveQueryFormTitle', savedQueryName); + } + + public async toggleIncludeFilters() { + await testSubjects.click('saveQueryFormIncludeFiltersOption'); + } + + public async saveCurrentSavedQuery() { + await testSubjects.click('savedQueryFormSaveButton'); + } + + public async deleteSavedQuery() { + await testSubjects.click('delete-saved-query-TEST-button'); + } + + public async confirmDeletionOfSavedQuery() { + await testSubjects.click('confirmModalConfirmButton'); + } + + public async clearSavedQuery() { + await testSubjects.click('saved-query-management-clear-button'); + } } return new DiscoverPage(); diff --git a/test/functional/services/toasts.ts b/test/functional/services/toasts.ts index a70e4ba464ae8..f5416a44e3b5a 100644 --- a/test/functional/services/toasts.ts +++ b/test/functional/services/toasts.ts @@ -63,7 +63,7 @@ export function ToastsProvider({ getService }: FtrProviderContext) { } } - private async getToastElement(index: number) { + public async getToastElement(index: number) { const list = await this.getGlobalToastList(); return await list.findByCssSelector(`.euiToast:nth-child(${index})`); } diff --git a/test/scripts/jenkins_build_plugins.sh b/test/scripts/jenkins_build_plugins.sh index 0c3ee4e3f261f..59df02d401167 100755 --- a/test/scripts/jenkins_build_plugins.sh +++ b/test/scripts/jenkins_build_plugins.sh @@ -5,7 +5,6 @@ source src/dev/ci_setup/setup_env.sh echo " -> building kibana platform plugins" node scripts/build_kibana_platform_plugins \ --oss \ - --filter '!alertingExample' \ --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \ --workers 6 \ diff --git a/vars/kibanaCoverage.groovy b/vars/kibanaCoverage.groovy index 66ebe3478fbec..e75ed8fef9875 100644 --- a/vars/kibanaCoverage.groovy +++ b/vars/kibanaCoverage.groovy @@ -169,31 +169,31 @@ def uploadCombinedReports() { ) } -def ingestData(jobName, buildNum, buildUrl, previousSha, title) { +def ingestData(jobName, buildNum, buildUrl, previousSha, teamAssignmentsPath, title) { kibanaPipeline.bash(""" source src/dev/ci_setup/setup_env.sh yarn kbn bootstrap --prefer-offline # Using existing target/kibana-coverage folder - . src/dev/code_coverage/shell_scripts/ingest_coverage.sh '${jobName}' ${buildNum} '${buildUrl}' ${previousSha} + . src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh '${jobName}' ${buildNum} '${buildUrl}' '${previousSha}' '${teamAssignmentsPath}' """, title) } -def ingestWithVault(jobName, buildNum, buildUrl, previousSha, title) { +def ingestWithVault(jobName, buildNum, buildUrl, previousSha, teamAssignmentsPath, title) { def vaultSecret = 'secret/kibana-issues/prod/coverage/elasticsearch' withVaultSecret(secret: vaultSecret, secret_field: 'host', variable_name: 'HOST_FROM_VAULT') { withVaultSecret(secret: vaultSecret, secret_field: 'username', variable_name: 'USER_FROM_VAULT') { withVaultSecret(secret: vaultSecret, secret_field: 'password', variable_name: 'PASS_FROM_VAULT') { - ingestData(jobName, buildNum, buildUrl, previousSha, title) + ingestData(jobName, buildNum, buildUrl, previousSha, teamAssignmentsPath, title) } } } } -def ingest(jobName, buildNumber, buildUrl, timestamp, previousSha, title) { +def ingest(jobName, buildNumber, buildUrl, timestamp, previousSha, teamAssignmentsPath, title) { withEnv([ "TIME_STAMP=${timestamp}", ]) { - ingestWithVault(jobName, buildNumber, buildUrl, previousSha, title) + ingestWithVault(jobName, buildNumber, buildUrl, previousSha, teamAssignmentsPath, title) } } diff --git a/vars/kibanaTeamAssign.groovy b/vars/kibanaTeamAssign.groovy index a3d9c16ef506e..caf1ee36e25a8 100644 --- a/vars/kibanaTeamAssign.groovy +++ b/vars/kibanaTeamAssign.groovy @@ -1,25 +1,13 @@ -def loadIngestionPipeline(ingestionPipelineName, title) { +def generateTeamAssignments(teamAssignmentsPath, title) { kibanaPipeline.bash(""" - source src/dev/ci_setup/setup_env.sh true + source src/dev/ci_setup/setup_env.sh yarn kbn bootstrap --prefer-offline - . src/dev/code_coverage/shell_scripts/assign_teams.sh '${ingestionPipelineName}' - """, title) -} + # Build team assignments dat file + node scripts/generate_team_assignments.js --verbose --dest '${teamAssignmentsPath}' -def loadWithVault(ingestionPipelineName, title) { - def vaultSecret = 'secret/kibana-issues/prod/coverage/elasticsearch' - withVaultSecret(secret: vaultSecret, secret_field: 'host', variable_name: 'HOST_FROM_VAULT') { - withVaultSecret(secret: vaultSecret, secret_field: 'username', variable_name: 'USER_FROM_VAULT') { - withVaultSecret(secret: vaultSecret, secret_field: 'password', variable_name: 'PASS_FROM_VAULT') { - loadIngestionPipeline(ingestionPipelineName, title) - } - } - } -} - -def load(ingestionPipelineName, title) { - loadWithVault(ingestionPipelineName, title) + cat '${teamAssignmentsPath}' + """, title) } return this diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 66ae478b86828..c37a3231b2705 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -2,10 +2,7 @@ "prefix": "xpack", "paths": { "xpack.actions": "plugins/actions", - "xpack.uiActionsEnhanced": [ - "plugins/ui_actions_enhanced", - "examples/ui_actions_enhanced_examples" - ], + "xpack.uiActionsEnhanced": "plugins/ui_actions_enhanced", "xpack.alerts": "plugins/alerts", "xpack.eventLog": "plugins/event_log", "xpack.alertingBuiltins": "plugins/alerting_builtins", @@ -59,6 +56,9 @@ "xpack.watcher": "plugins/watcher", "xpack.observability": "plugins/observability" }, + "exclude": [ + "examples" + ], "translations": [ "plugins/translations/translations/zh-CN.json", "plugins/translations/translations/ja-JP.json" diff --git a/examples/alerting_example/README.md b/x-pack/examples/alerting_example/README.md similarity index 100% rename from examples/alerting_example/README.md rename to x-pack/examples/alerting_example/README.md diff --git a/x-pack/examples/alerting_example/common/constants.ts b/x-pack/examples/alerting_example/common/constants.ts new file mode 100644 index 0000000000000..dd9cc21954e61 --- /dev/null +++ b/x-pack/examples/alerting_example/common/constants.ts @@ -0,0 +1,21 @@ +/* + * 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 ALERTING_EXAMPLE_APP_ID = 'AlertingExample'; + +// always firing +export const DEFAULT_INSTANCES_TO_GENERATE = 5; + +// Astros +export enum Craft { + OuterSpace = 'Outer Space', + ISS = 'ISS', +} +export enum Operator { + AreAbove = 'Are above', + AreBelow = 'Are below', + AreExactly = 'Are exactly', +} diff --git a/examples/alerting_example/kibana.json b/x-pack/examples/alerting_example/kibana.json similarity index 100% rename from examples/alerting_example/kibana.json rename to x-pack/examples/alerting_example/kibana.json diff --git a/examples/alerting_example/public/alert_types/always_firing.tsx b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx similarity index 69% rename from examples/alerting_example/public/alert_types/always_firing.tsx rename to x-pack/examples/alerting_example/public/alert_types/always_firing.tsx index 130519308d3c3..839669bda1098 100644 --- a/examples/alerting_example/public/alert_types/always_firing.tsx +++ b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx @@ -1,26 +1,13 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * 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, { Fragment } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFieldNumber, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AlertTypeModel } from '../../../../x-pack/plugins/triggers_actions_ui/public'; +import { AlertTypeModel } from '../../../../plugins/triggers_actions_ui/public'; import { DEFAULT_INSTANCES_TO_GENERATE } from '../../common/constants'; interface AlwaysFiringParamsProps { diff --git a/examples/alerting_example/public/alert_types/astros.tsx b/x-pack/examples/alerting_example/public/alert_types/astros.tsx similarity index 89% rename from examples/alerting_example/public/alert_types/astros.tsx rename to x-pack/examples/alerting_example/public/alert_types/astros.tsx index d52223cb6b988..4f894cfe231c9 100644 --- a/examples/alerting_example/public/alert_types/astros.tsx +++ b/x-pack/examples/alerting_example/public/alert_types/astros.tsx @@ -1,20 +1,7 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ import React, { useState, useEffect, Fragment } from 'react'; @@ -32,9 +19,9 @@ import { import { i18n } from '@kbn/i18n'; import { flatten } from 'lodash'; import { ALERTING_EXAMPLE_APP_ID, Craft, Operator } from '../../common/constants'; -import { SanitizedAlert } from '../../../../x-pack/plugins/alerts/common'; -import { PluginSetupContract as AlertingSetup } from '../../../../x-pack/plugins/alerts/public'; -import { AlertTypeModel } from '../../../../x-pack/plugins/triggers_actions_ui/public'; +import { SanitizedAlert } from '../../../../plugins/alerts/common'; +import { PluginSetupContract as AlertingSetup } from '../../../../plugins/alerts/public'; +import { AlertTypeModel } from '../../../../plugins/triggers_actions_ui/public'; export function registerNavigation(alerts: AlertingSetup) { alerts.registerNavigation( diff --git a/x-pack/examples/alerting_example/public/alert_types/index.ts b/x-pack/examples/alerting_example/public/alert_types/index.ts new file mode 100644 index 0000000000000..f9a97f43e116a --- /dev/null +++ b/x-pack/examples/alerting_example/public/alert_types/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { registerNavigation as registerPeopleInSpaceNavigation } from './astros'; +import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; +import { SanitizedAlert } from '../../../../plugins/alerts/common'; +import { PluginSetupContract as AlertingSetup } from '../../../../plugins/alerts/public'; + +export function registerNavigation(alerts: AlertingSetup) { + // register default navigation + alerts.registerDefaultNavigation( + ALERTING_EXAMPLE_APP_ID, + (alert: SanitizedAlert) => `/alert/${alert.id}` + ); + + registerPeopleInSpaceNavigation(alerts); +} diff --git a/examples/alerting_example/public/application.tsx b/x-pack/examples/alerting_example/public/application.tsx similarity index 73% rename from examples/alerting_example/public/application.tsx rename to x-pack/examples/alerting_example/public/application.tsx index 23e9d19441002..ebffc7d038aef 100644 --- a/examples/alerting_example/public/application.tsx +++ b/x-pack/examples/alerting_example/public/application.tsx @@ -1,20 +1,7 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * 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'; @@ -28,14 +15,14 @@ import { DocLinksStart, ToastsSetup, ApplicationStart, -} from '../../../src/core/public'; -import { DataPublicPluginStart } from '../../../src/plugins/data/public'; -import { ChartsPluginStart } from '../../../src/plugins/charts/public'; +} from '../../../../src/core/public'; +import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; import { Page } from './components/page'; import { DocumentationPage } from './components/documentation'; import { ViewAlertPage } from './components/view_alert'; -import { TriggersAndActionsUIPublicPluginStart } from '../../../x-pack/plugins/triggers_actions_ui/public'; +import { TriggersAndActionsUIPublicPluginStart } from '../../../plugins/triggers_actions_ui/public'; import { AlertingExamplePublicStartDeps } from './plugin'; import { ViewPeopleInSpaceAlertPage } from './components/view_astros_alert'; diff --git a/examples/alerting_example/public/components/create_alert.tsx b/x-pack/examples/alerting_example/public/components/create_alert.tsx similarity index 64% rename from examples/alerting_example/public/components/create_alert.tsx rename to x-pack/examples/alerting_example/public/components/create_alert.tsx index 72e3835b100fe..c75c230e4f04e 100644 --- a/examples/alerting_example/public/components/create_alert.tsx +++ b/x-pack/examples/alerting_example/public/components/create_alert.tsx @@ -1,30 +1,14 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ import React, { useState } from 'react'; import { EuiIcon, EuiFlexItem, EuiCard, EuiFlexGroup } from '@elastic/eui'; -import { - AlertsContextProvider, - AlertAdd, -} from '../../../../x-pack/plugins/triggers_actions_ui/public'; +import { AlertsContextProvider, AlertAdd } from '../../../../plugins/triggers_actions_ui/public'; import { AlertingExampleComponentParams } from '../application'; import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; diff --git a/examples/alerting_example/public/components/documentation.tsx b/x-pack/examples/alerting_example/public/components/documentation.tsx similarity index 63% rename from examples/alerting_example/public/components/documentation.tsx rename to x-pack/examples/alerting_example/public/components/documentation.tsx index 17cc34959b010..73896fdb8fc92 100644 --- a/examples/alerting_example/public/components/documentation.tsx +++ b/x-pack/examples/alerting_example/public/components/documentation.tsx @@ -1,21 +1,9 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * 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 { diff --git a/examples/alerting_example/public/components/page.tsx b/x-pack/examples/alerting_example/public/components/page.tsx similarity index 61% rename from examples/alerting_example/public/components/page.tsx rename to x-pack/examples/alerting_example/public/components/page.tsx index 99076c7ddcedf..e7fba53d7a333 100644 --- a/examples/alerting_example/public/components/page.tsx +++ b/x-pack/examples/alerting_example/public/components/page.tsx @@ -1,20 +1,7 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * 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'; diff --git a/examples/alerting_example/public/components/view_alert.tsx b/x-pack/examples/alerting_example/public/components/view_alert.tsx similarity index 79% rename from examples/alerting_example/public/components/view_alert.tsx rename to x-pack/examples/alerting_example/public/components/view_alert.tsx index 0f7fc70648a9e..6d71872d5d3f2 100644 --- a/examples/alerting_example/public/components/view_alert.tsx +++ b/x-pack/examples/alerting_example/public/components/view_alert.tsx @@ -1,21 +1,9 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ + import React, { useState, useEffect, Fragment } from 'react'; import { @@ -32,11 +20,7 @@ import { import { withRouter, RouteComponentProps } from 'react-router-dom'; import { CoreStart } from 'kibana/public'; import { isEmpty } from 'lodash'; -import { - Alert, - AlertTaskState, - BASE_ALERT_API_PATH, -} from '../../../../x-pack/plugins/alerts/common'; +import { Alert, AlertTaskState, BASE_ALERT_API_PATH } from '../../../../plugins/alerts/common'; import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; type Props = RouteComponentProps & { diff --git a/examples/alerting_example/public/components/view_astros_alert.tsx b/x-pack/examples/alerting_example/public/components/view_astros_alert.tsx similarity index 79% rename from examples/alerting_example/public/components/view_astros_alert.tsx rename to x-pack/examples/alerting_example/public/components/view_astros_alert.tsx index b2d3cec269b72..e4687c75fa0b7 100644 --- a/examples/alerting_example/public/components/view_astros_alert.tsx +++ b/x-pack/examples/alerting_example/public/components/view_astros_alert.tsx @@ -1,21 +1,9 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ + import React, { useState, useEffect, Fragment } from 'react'; import { @@ -34,11 +22,7 @@ import { import { withRouter, RouteComponentProps } from 'react-router-dom'; import { CoreStart } from 'kibana/public'; import { isEmpty } from 'lodash'; -import { - Alert, - AlertTaskState, - BASE_ALERT_API_PATH, -} from '../../../../x-pack/plugins/alerts/common'; +import { Alert, AlertTaskState, BASE_ALERT_API_PATH } from '../../../../plugins/alerts/common'; import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; type Props = RouteComponentProps & { diff --git a/x-pack/examples/alerting_example/public/index.ts b/x-pack/examples/alerting_example/public/index.ts new file mode 100644 index 0000000000000..9e44d167184a9 --- /dev/null +++ b/x-pack/examples/alerting_example/public/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. + */ + +import { AlertingExamplePlugin } from './plugin'; + +export const plugin = () => new AlertingExamplePlugin(); diff --git a/examples/alerting_example/public/plugin.tsx b/x-pack/examples/alerting_example/public/plugin.tsx similarity index 63% rename from examples/alerting_example/public/plugin.tsx rename to x-pack/examples/alerting_example/public/plugin.tsx index 3f972fa9fe2ee..b1f8b6b99ece1 100644 --- a/examples/alerting_example/public/plugin.tsx +++ b/x-pack/examples/alerting_example/public/plugin.tsx @@ -1,31 +1,23 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * 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 { Plugin, CoreSetup, AppMountParameters, AppNavLinkStatus } from '../../../src/core/public'; -import { PluginSetupContract as AlertingSetup } from '../../../x-pack/plugins/alerts/public'; -import { ChartsPluginStart } from '../../../src/plugins/charts/public'; -import { TriggersAndActionsUIPublicPluginSetup } from '../../../x-pack/plugins/triggers_actions_ui/public'; -import { DataPublicPluginStart } from '../../../src/plugins/data/public'; +import { + Plugin, + CoreSetup, + AppMountParameters, + AppNavLinkStatus, +} from '../../../../src/core/public'; +import { PluginSetupContract as AlertingSetup } from '../../../plugins/alerts/public'; +import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; +import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public'; +import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { getAlertType as getAlwaysFiringAlertType } from './alert_types/always_firing'; import { getAlertType as getPeopleInSpaceAlertType } from './alert_types/astros'; import { registerNavigation } from './alert_types'; -import { DeveloperExamplesSetup } from '../../developer_examples/public'; +import { DeveloperExamplesSetup } from '../../../../examples/developer_examples/public'; export type Setup = void; export type Start = void; diff --git a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts new file mode 100644 index 0000000000000..bb1cb0d97689b --- /dev/null +++ b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import uuid from 'uuid'; +import { range } from 'lodash'; +import { AlertType } from '../../../../plugins/alerts/server'; +import { DEFAULT_INSTANCES_TO_GENERATE, ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; + +export const alertType: AlertType = { + id: 'example.always-firing', + name: 'Always firing', + actionGroups: [{ id: 'default', name: 'default' }], + defaultActionGroupId: 'default', + async executor({ services, params: { instances = DEFAULT_INSTANCES_TO_GENERATE }, state }) { + const count = (state.count ?? 0) + 1; + + range(instances) + .map(() => ({ id: uuid.v4() })) + .forEach((instance: { id: string }) => { + services + .alertInstanceFactory(instance.id) + .replaceState({ triggerdOnCycle: count }) + .scheduleActions('default'); + }); + + return { + count, + }; + }, + producer: ALERTING_EXAMPLE_APP_ID, +}; diff --git a/examples/alerting_example/server/alert_types/astros.ts b/x-pack/examples/alerting_example/server/alert_types/astros.ts similarity index 67% rename from examples/alerting_example/server/alert_types/astros.ts rename to x-pack/examples/alerting_example/server/alert_types/astros.ts index 1ccf8af4ce623..852e6f57d1106 100644 --- a/examples/alerting_example/server/alert_types/astros.ts +++ b/x-pack/examples/alerting_example/server/alert_types/astros.ts @@ -1,24 +1,11 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * 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 axios from 'axios'; -import { AlertType } from '../../../../x-pack/plugins/alerts/server'; +import { AlertType } from '../../../../plugins/alerts/server'; import { Operator, Craft, ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; interface PeopleInSpace { diff --git a/x-pack/examples/alerting_example/server/index.ts b/x-pack/examples/alerting_example/server/index.ts new file mode 100644 index 0000000000000..95676bec73555 --- /dev/null +++ b/x-pack/examples/alerting_example/server/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { PluginInitializer } from 'kibana/server'; +import { AlertingExamplePlugin } from './plugin'; + +export const plugin: PluginInitializer = () => new AlertingExamplePlugin(); diff --git a/examples/alerting_example/server/plugin.ts b/x-pack/examples/alerting_example/server/plugin.ts similarity index 64% rename from examples/alerting_example/server/plugin.ts rename to x-pack/examples/alerting_example/server/plugin.ts index 4141b48ffeeaf..69971846e32b0 100644 --- a/examples/alerting_example/server/plugin.ts +++ b/x-pack/examples/alerting_example/server/plugin.ts @@ -1,31 +1,18 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * 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 { Plugin, CoreSetup } from 'kibana/server'; import { i18n } from '@kbn/i18n'; -import { DEFAULT_APP_CATEGORIES } from '../../../src/core/server'; -import { PluginSetupContract as AlertingSetup } from '../../../x-pack/plugins/alerts/server'; -import { PluginSetupContract as FeaturesPluginSetup } from '../../../x-pack/plugins/features/server'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; +import { PluginSetupContract as AlertingSetup } from '../../../plugins/alerts/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../../plugins/features/server'; import { alertType as alwaysFiringAlert } from './alert_types/always_firing'; import { alertType as peopleInSpaceAlert } from './alert_types/astros'; -import { INDEX_THRESHOLD_ID } from '../../../x-pack/plugins/alerting_builtins/server'; +import { INDEX_THRESHOLD_ID } from '../../../plugins/alerting_builtins/server'; import { ALERTING_EXAMPLE_APP_ID } from '../common/constants'; // this plugin's dependendencies diff --git a/examples/alerting_example/tsconfig.json b/x-pack/examples/alerting_example/tsconfig.json similarity index 64% rename from examples/alerting_example/tsconfig.json rename to x-pack/examples/alerting_example/tsconfig.json index 214e4b78a9a70..95d42d40aceb3 100644 --- a/examples/alerting_example/tsconfig.json +++ b/x-pack/examples/alerting_example/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target" }, @@ -9,10 +9,10 @@ "public/**/*.tsx", "server/**/*.ts", "common/**/*.ts", - "../../typings/**/*", + "../../../typings/**/*", ], "exclude": [], "references": [ - { "path": "../../src/core/tsconfig.json" } + { "path": "../../../src/core/tsconfig.json" } ] } diff --git a/x-pack/package.json b/x-pack/package.json index 984843e5b6360..482e832aea916 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -80,7 +80,7 @@ "@types/geojson": "7946.0.7", "@types/getos": "^3.0.0", "@types/git-url-parse": "^9.0.0", - "@types/glob": "^7.1.1", + "@types/glob": "^7.1.2", "@types/graphql": "^0.13.2", "@types/gulp": "^4.0.6", "@types/hapi__wreck": "^15.0.1", @@ -107,7 +107,7 @@ "@types/nodemailer": "^6.2.1", "@types/object-hash": "^1.3.0", "@types/papaparse": "^5.0.3", - "@types/pngjs": "^3.3.2", + "@types/pngjs": "^3.4.0", "@types/pretty-ms": "^5.0.0", "@types/prop-types": "^15.7.3", "@types/proper-lockfile": "^3.0.1", @@ -139,12 +139,11 @@ "@welldone-software/why-did-you-render": "^4.0.0", "abab": "^1.0.4", "angular": "^1.8.0", - "angular-sanitize": "1.8.0", + "angular-sanitize": "^1.8.0", "apollo-link": "^1.2.3", "apollo-link-error": "^1.1.7", "apollo-link-state": "^0.4.1", "autoprefixer": "^9.7.4", - "axios": "^0.19.0", "babel-jest": "^25.5.1", "babel-loader": "^8.0.6", "babel-plugin-require-context-hook": "npm:babel-plugin-require-context-hook-babel7@1.0.0", @@ -156,7 +155,7 @@ "chalk": "^4.1.0", "chance": "1.0.18", "cheerio": "0.22.0", - "commander": "3.0.2", + "commander": "^3.0.2", "constate": "^1.3.2", "copy-to-clipboard": "^3.0.8", "copy-webpack-plugin": "^6.0.2", @@ -211,7 +210,7 @@ "mocha-junit-reporter": "^1.23.1", "mochawesome": "^4.1.0", "mochawesome-merge": "^4.1.0", - "mustache": "^2.3.0", + "mustache": "^2.3.2", "mutation-observer": "^1.0.3", "null-loader": "^3.0.0", "oboe": "^2.1.4", @@ -257,7 +256,6 @@ "supertest-as-promised": "^4.0.2", "suricata-sid-db": "^1.0.2", "tinycolor2": "1.4.1", - "tmp": "0.1.0", "topojson-client": "3.0.0", "tree-kill": "^1.2.2", "ts-loader": "^6.0.4", @@ -298,8 +296,8 @@ "apollo-link-schema": "^1.1.0", "apollo-server-errors": "^2.0.2", "apollo-server-hapi": "^1.3.6", - "archiver": "3.1.1", - "axios": "^0.19.0", + "archiver": "^3.1.1", + "axios": "^0.19.2", "bluebird": "3.5.5", "boom": "^7.2.0", "chroma-js": "^1.4.1", @@ -328,7 +326,7 @@ "graphql-tools": "^3.0.2", "h2o2": "^8.1.2", "handlebars": "4.7.6", - "history": "4.9.0", + "history": "^4.9.0", "idx": "^2.5.6", "immer": "^1.5.0", "inline-style": "^2.0.0", @@ -356,13 +354,13 @@ "p-retry": "^4.2.0", "papaparse": "^5.2.0", "pdfmake": "^0.1.65", - "pngjs": "3.4.0", + "pngjs": "^3.4.0", "prop-types": "^15.7.2", "proper-lockfile": "^3.2.0", "puid": "1.0.7", "puppeteer-core": "^1.19.0", "query-string": "^6.13.2", - "raw-loader": "3.1.0", + "raw-loader": "^3.1.0", "react": "^16.12.0", "react-datetime": "^2.14.0", "react-dom": "^16.12.0", @@ -377,7 +375,7 @@ "request": "^2.88.0", "rison-node": "0.3.1", "rxjs": "^6.5.5", - "semver": "5.7.0", + "semver": "^5.7.0", "set-value": "^3.0.2", "squel": "^5.13.0", "stats-lite": "^2.2.0", diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts index 772e7df416979..e406b37ae61fb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts @@ -169,6 +169,16 @@ describe('validateParams()', () => { }); }).toThrowError(`error validating action params: error parsing timestamp "${timestamp}"`); }); + + test('should validate and throw error when dedupKey is missing on resolve', () => { + expect(() => { + validateParams(actionType, { + eventAction: 'resolve', + }); + }).toThrowError( + `error validating action params: DedupKey is required when eventAction is "resolve"` + ); + }); }); describe('execute()', () => { @@ -199,7 +209,6 @@ describe('execute()', () => { Object { "apiUrl": "https://events.pagerduty.com/v2/enqueue", "data": Object { - "dedup_key": "action:some-action-id", "event_action": "trigger", "payload": Object { "severity": "info", @@ -509,4 +518,61 @@ describe('execute()', () => { } `); }); + + test('should not set a default dedupkey to ensure each execution is a unique PagerDuty incident', async () => { + const randoDate = new Date('1963-09-23T01:23:45Z').toISOString(); + const secrets = { + routingKey: 'super-secret', + }; + const config = { + apiUrl: 'the-api-url', + }; + const params: ActionParamsType = { + eventAction: 'trigger', + summary: 'the summary', + source: 'the-source', + severity: 'critical', + timestamp: randoDate, + }; + + postPagerdutyMock.mockImplementation(() => { + return { status: 202, data: 'data-here' }; + }); + + const actionId = 'some-action-id'; + const executorOptions: PagerDutyActionTypeExecutorOptions = { + actionId, + config, + params, + secrets, + services, + }; + const actionResponse = await actionType.executor(executorOptions); + const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0]; + expect({ apiUrl, data, headers }).toMatchInlineSnapshot(` + Object { + "apiUrl": "the-api-url", + "data": Object { + "event_action": "trigger", + "payload": Object { + "severity": "critical", + "source": "the-source", + "summary": "the summary", + "timestamp": "1963-09-23T01:23:45.000Z", + }, + }, + "headers": Object { + "Content-Type": "application/json", + "X-Routing-Key": "super-secret", + }, + } + `); + expect(actionResponse).toMatchInlineSnapshot(` + Object { + "actionId": "some-action-id", + "data": "data-here", + "status": "ok", + } + `); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts index 640a38d77b6c2..4574b748e6014 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { curry } from 'lodash'; +import { curry, isUndefined, pick, omitBy } from 'lodash'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import { postPagerduty } from './lib/post_pagerduty'; @@ -51,6 +51,10 @@ export type ActionParamsType = TypeOf; const EVENT_ACTION_TRIGGER = 'trigger'; const EVENT_ACTION_RESOLVE = 'resolve'; const EVENT_ACTION_ACKNOWLEDGE = 'acknowledge'; +const EVENT_ACTIONS_WITH_REQUIRED_DEDUPKEY = new Set([ + EVENT_ACTION_RESOLVE, + EVENT_ACTION_ACKNOWLEDGE, +]); const EventActionSchema = schema.oneOf([ schema.literal(EVENT_ACTION_TRIGGER), @@ -81,7 +85,7 @@ const ParamsSchema = schema.object( ); function validateParams(paramsObject: unknown): string | void { - const { timestamp } = paramsObject as ActionParamsType; + const { timestamp, eventAction, dedupKey } = paramsObject as ActionParamsType; if (timestamp != null) { try { const date = Date.parse(timestamp); @@ -103,6 +107,14 @@ function validateParams(paramsObject: unknown): string | void { }); } } + if (eventAction && EVENT_ACTIONS_WITH_REQUIRED_DEDUPKEY.has(eventAction) && !dedupKey) { + return i18n.translate('xpack.actions.builtin.pagerduty.missingDedupkeyErrorMessage', { + defaultMessage: `DedupKey is required when eventAction is "{eventAction}"`, + values: { + eventAction, + }, + }); + } } // action type definition @@ -230,26 +242,29 @@ async function executor( const AcknowledgeOrResolve = new Set([EVENT_ACTION_ACKNOWLEDGE, EVENT_ACTION_RESOLVE]); -function getBodyForEventAction(actionId: string, params: ActionParamsType): unknown { - const eventAction = params.eventAction || EVENT_ACTION_TRIGGER; - const dedupKey = params.dedupKey || `action:${actionId}`; - - const data: { - event_action: ActionParamsType['eventAction']; - dedup_key: string; - payload?: { - summary: string; - source: string; - severity: string; - timestamp?: string; - component?: string; - group?: string; - class?: string; - }; - } = { +interface PagerDutyPayload { + event_action: ActionParamsType['eventAction']; + dedup_key?: string; + payload?: { + summary: string; + source: string; + severity: string; + timestamp?: string; + component?: string; + group?: string; + class?: string; + }; +} + +function getBodyForEventAction(actionId: string, params: ActionParamsType): PagerDutyPayload { + const eventAction = params.eventAction ?? EVENT_ACTION_TRIGGER; + + const data: PagerDutyPayload = { event_action: eventAction, - dedup_key: dedupKey, }; + if (params.dedupKey) { + data.dedup_key = params.dedupKey; + } // for acknowledge / resolve, just send the dedup key if (AcknowledgeOrResolve.has(eventAction)) { @@ -260,12 +275,8 @@ function getBodyForEventAction(actionId: string, params: ActionParamsType): unkn summary: params.summary || 'No summary provided.', source: params.source || `Kibana Action ${actionId}`, severity: params.severity || 'info', + ...omitBy(pick(params, ['timestamp', 'component', 'group', 'class']), isUndefined), }; - if (params.timestamp != null) data.payload.timestamp = params.timestamp; - if (params.component != null) data.payload.component = params.component; - if (params.group != null) data.payload.group = params.group; - if (params.class != null) data.payload.class = params.class; - return data; } diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index a6cffb0284815..d4817eab64acb 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -1696,14 +1696,22 @@ describe('muteAll()', () => { muteAll: false, }, references: [], + version: '123', }); await alertsClient.muteAll({ id: '1' }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { - muteAll: true, - mutedInstanceIds: [], - updatedBy: 'elastic', - }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + muteAll: true, + mutedInstanceIds: [], + updatedBy: 'elastic', + }, + { + version: '123', + } + ); }); describe('authorization', () => { @@ -1785,11 +1793,18 @@ describe('unmuteAll()', () => { }); await alertsClient.unmuteAll({ id: '1' }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { - muteAll: false, - mutedInstanceIds: [], - updatedBy: 'elastic', - }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + muteAll: false, + mutedInstanceIds: [], + updatedBy: 'elastic', + }, + { + version: '123', + } + ); }); describe('authorization', () => { diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 671b1d6411d7f..033fdd752c695 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -45,6 +45,8 @@ import { parseIsoOrRelativeDate } from './lib/iso_or_relative_date'; import { alertInstanceSummaryFromEventLog } from './lib/alert_instance_summary_from_event_log'; import { IEvent } from '../../event_log/server'; import { parseDuration } from '../common/parse_duration'; +import { retryIfConflicts } from './lib/retry_if_conflicts'; +import { partiallyUpdateAlert } from './saved_objects'; export interface RegistryAlertTypeWithAuth extends RegistryAlertType { authorizedConsumers: string[]; @@ -421,6 +423,14 @@ export class AlertsClient { } public async update({ id, data }: UpdateOptions): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.update('${id}')`, + async () => await this.updateWithOCC({ id, data }) + ); + } + + private async updateWithOCC({ id, data }: UpdateOptions): Promise { let alertSavedObject: SavedObject; try { @@ -529,7 +539,15 @@ export class AlertsClient { }; } - public async updateApiKey({ id }: { id: string }) { + public async updateApiKey({ id }: { id: string }): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.updateApiKey('${id}')`, + async () => await this.updateApiKeyWithOCC({ id }) + ); + } + + private async updateApiKeyWithOCC({ id }: { id: string }) { let apiKeyToInvalidate: string | null = null; let attributes: RawAlert; let version: string | undefined; @@ -597,7 +615,15 @@ export class AlertsClient { } } - public async enable({ id }: { id: string }) { + public async enable({ id }: { id: string }): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.enable('${id}')`, + async () => await this.enableWithOCC({ id }) + ); + } + + private async enableWithOCC({ id }: { id: string }) { let apiKeyToInvalidate: string | null = null; let attributes: RawAlert; let version: string | undefined; @@ -658,7 +684,15 @@ export class AlertsClient { } } - public async disable({ id }: { id: string }) { + public async disable({ id }: { id: string }): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.disable('${id}')`, + async () => await this.disableWithOCC({ id }) + ); + } + + private async disableWithOCC({ id }: { id: string }) { let apiKeyToInvalidate: string | null = null; let attributes: RawAlert; let version: string | undefined; @@ -711,8 +745,19 @@ export class AlertsClient { } } - public async muteAll({ id }: { id: string }) { - const { attributes } = await this.unsecuredSavedObjectsClient.get('alert', id); + public async muteAll({ id }: { id: string }): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.muteAll('${id}')`, + async () => await this.muteAllWithOCC({ id }) + ); + } + + private async muteAllWithOCC({ id }: { id: string }) { + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + 'alert', + id + ); await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, @@ -723,19 +768,34 @@ export class AlertsClient { await this.actionsAuthorization.ensureAuthorized('execute'); } - await this.unsecuredSavedObjectsClient.update( - 'alert', + const updateAttributes = this.updateMeta({ + muteAll: true, + mutedInstanceIds: [], + updatedBy: await this.getUserName(), + }); + const updateOptions = { version }; + + await partiallyUpdateAlert( + this.unsecuredSavedObjectsClient, id, - this.updateMeta({ - muteAll: true, - mutedInstanceIds: [], - updatedBy: await this.getUserName(), - }) + updateAttributes, + updateOptions + ); + } + + public async unmuteAll({ id }: { id: string }): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.unmuteAll('${id}')`, + async () => await this.unmuteAllWithOCC({ id }) ); } - public async unmuteAll({ id }: { id: string }) { - const { attributes } = await this.unsecuredSavedObjectsClient.get('alert', id); + private async unmuteAllWithOCC({ id }: { id: string }) { + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + 'alert', + id + ); await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, @@ -746,18 +806,30 @@ export class AlertsClient { await this.actionsAuthorization.ensureAuthorized('execute'); } - await this.unsecuredSavedObjectsClient.update( - 'alert', + const updateAttributes = this.updateMeta({ + muteAll: false, + mutedInstanceIds: [], + updatedBy: await this.getUserName(), + }); + const updateOptions = { version }; + + await partiallyUpdateAlert( + this.unsecuredSavedObjectsClient, id, - this.updateMeta({ - muteAll: false, - mutedInstanceIds: [], - updatedBy: await this.getUserName(), - }) + updateAttributes, + updateOptions + ); + } + + public async muteInstance({ alertId, alertInstanceId }: MuteOptions): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.muteInstance('${alertId}')`, + async () => await this.muteInstanceWithOCC({ alertId, alertInstanceId }) ); } - public async muteInstance({ alertId, alertInstanceId }: MuteOptions) { + private async muteInstanceWithOCC({ alertId, alertInstanceId }: MuteOptions) { const { attributes, version } = await this.unsecuredSavedObjectsClient.get( 'alert', alertId @@ -788,7 +860,15 @@ export class AlertsClient { } } - public async unmuteInstance({ + public async unmuteInstance({ alertId, alertInstanceId }: MuteOptions): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.unmuteInstance('${alertId}')`, + async () => await this.unmuteInstanceWithOCC({ alertId, alertInstanceId }) + ); + } + + private async unmuteInstanceWithOCC({ alertId, alertInstanceId, }: { diff --git a/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts b/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts new file mode 100644 index 0000000000000..1c5edb45c80fe --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts @@ -0,0 +1,359 @@ +/* + * 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 { cloneDeep } from 'lodash'; + +import { AlertsClient, ConstructorOptions } from './alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; +import { alertTypeRegistryMock } from './alert_type_registry.mock'; +import { alertsAuthorizationMock } from './authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; +import { actionsClientMock, actionsAuthorizationMock } from '../../actions/server/mocks'; +import { AlertsAuthorization } from './authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../actions/server'; +import { SavedObjectsErrorHelpers } from '../../../../src/core/server'; +import { RetryForConflictsAttempts } from './lib/retry_if_conflicts'; +import { TaskStatus } from '../../../plugins/task_manager/server/task'; + +let alertsClient: AlertsClient; + +const MockAlertId = 'alert-id'; + +const ConflictAfterRetries = RetryForConflictsAttempts + 1; + +const taskManager = taskManagerMock.start(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const logger = loggingSystemMock.create().get(); +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger, + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +// this suite consists of two suites running tests against mutable alertsClient APIs: +// - one to run tests where an SO update conflicts once +// - one to run tests where an SO update conflicts too many times +describe('alerts_client_conflict_retries', () => { + // tests that mutable operations work if only one SO conflict occurs + describe(`1 retry works for method`, () => { + beforeEach(() => { + mockSavedObjectUpdateConflictErrorTimes(1); + }); + + testFn(update, true); + testFn(updateApiKey, true); + testFn(enable, true); + testFn(disable, true); + testFn(muteAll, true); + testFn(unmuteAll, true); + testFn(muteInstance, true); + testFn(unmuteInstance, true); + }); + + // tests that mutable operations fail if too many SO conflicts occurs + describe(`${ConflictAfterRetries} retries fails with conflict error`, () => { + beforeEach(() => { + mockSavedObjectUpdateConflictErrorTimes(ConflictAfterRetries); + }); + + testFn(update, false); + testFn(updateApiKey, false); + testFn(enable, false); + testFn(disable, false); + testFn(muteAll, false); + testFn(unmuteAll, false); + testFn(muteInstance, false); + testFn(unmuteInstance, false); + }); +}); + +// alertsClients methods being tested +// - success is passed as an indication if the alertsClient method +// is expected to succeed or not, based on the number of conflicts +// set up in the `beforeEach()` method + +async function update(success: boolean) { + try { + await alertsClient.update({ + id: MockAlertId, + data: { + schedule: { interval: '5s' }, + name: 'cba', + tags: ['bar'], + params: { bar: true }, + throttle: '10s', + actions: [], + }, + }); + } catch (err) { + // only checking the warn messages in this test + expect(logger.warn).lastCalledWith( + `alertsClient.update('alert-id') conflict, exceeded retries` + ); + return expectConflict(success, err, 'create'); + } + expectSuccess(success, 2, 'create'); + + // only checking the debug messages in this test + expect(logger.debug).nthCalledWith(1, `alertsClient.update('alert-id') conflict, retrying ...`); +} + +async function updateApiKey(success: boolean) { + try { + await alertsClient.updateApiKey({ id: MockAlertId }); + } catch (err) { + return expectConflict(success, err); + } + + expectSuccess(success); +} + +async function enable(success: boolean) { + setupRawAlertMocks({}, { enabled: false }); + + try { + await alertsClient.enable({ id: MockAlertId }); + } catch (err) { + return expectConflict(success, err); + } + + // a successful enable call makes 2 calls to update, so that's 3 total, + // 1 with conflict + 2 on success + expectSuccess(success, 3); +} + +async function disable(success: boolean) { + try { + await alertsClient.disable({ id: MockAlertId }); + } catch (err) { + return expectConflict(success, err); + } + + expectSuccess(success); +} + +async function muteAll(success: boolean) { + try { + await alertsClient.muteAll({ id: MockAlertId }); + } catch (err) { + return expectConflict(success, err); + } + + expectSuccess(success); +} + +async function unmuteAll(success: boolean) { + try { + await alertsClient.unmuteAll({ id: MockAlertId }); + } catch (err) { + return expectConflict(success, err); + } + + expectSuccess(success); +} + +async function muteInstance(success: boolean) { + try { + await alertsClient.muteInstance({ alertId: MockAlertId, alertInstanceId: 'instance-id' }); + } catch (err) { + return expectConflict(success, err); + } + + expectSuccess(success); +} + +async function unmuteInstance(success: boolean) { + setupRawAlertMocks({}, { mutedInstanceIds: ['instance-id'] }); + try { + await alertsClient.unmuteInstance({ alertId: MockAlertId, alertInstanceId: 'instance-id' }); + } catch (err) { + return expectConflict(success, err); + } + + expectSuccess(success); +} + +// tests to run when the method is expected to succeed +function expectSuccess( + success: boolean, + count: number = 2, + method: 'update' | 'create' = 'update' +) { + expect(success).toBe(true); + expect(unsecuredSavedObjectsClient[method]).toHaveBeenCalledTimes(count); + // message content checked in the update test + expect(logger.debug).toHaveBeenCalled(); +} + +// tests to run when the method is expected to fail +function expectConflict(success: boolean, err: Error, method: 'update' | 'create' = 'update') { + const conflictErrorMessage = SavedObjectsErrorHelpers.createConflictError('alert', MockAlertId) + .message; + + expect(`${err}`).toBe(`Error: ${conflictErrorMessage}`); + expect(success).toBe(false); + expect(unsecuredSavedObjectsClient[method]).toHaveBeenCalledTimes(ConflictAfterRetries); + // message content checked in the update test + expect(logger.debug).toBeCalledTimes(RetryForConflictsAttempts); + expect(logger.warn).toBeCalledTimes(1); +} + +// wrapper to call the test function with a it's own name +function testFn(fn: (success: boolean) => unknown, success: boolean) { + test(`${fn.name}`, async () => await fn(success)); +} + +// set up mocks for update or create (the update() method uses create!) +function mockSavedObjectUpdateConflictErrorTimes(times: number) { + // default success value + const mockUpdateValue = { + id: MockAlertId, + type: 'alert', + attributes: { + actions: [], + scheduledTaskId: 'scheduled-task-id', + }, + references: [], + }; + + unsecuredSavedObjectsClient.update.mockResolvedValue(mockUpdateValue); + unsecuredSavedObjectsClient.create.mockResolvedValue(mockUpdateValue); + + // queue up specified number of errors before a success call + for (let i = 0; i < times; i++) { + unsecuredSavedObjectsClient.update.mockRejectedValueOnce( + SavedObjectsErrorHelpers.createConflictError('alert', MockAlertId) + ); + unsecuredSavedObjectsClient.create.mockRejectedValueOnce( + SavedObjectsErrorHelpers.createConflictError('alert', MockAlertId) + ); + } +} + +// set up mocks needed to get the tested methods to run +function setupRawAlertMocks( + overrides: Record = {}, + attributeOverrides: Record = {} +) { + const rawAlert = { + id: MockAlertId, + type: 'alert', + attributes: { + enabled: true, + tags: ['foo'], + alertTypeId: 'myType', + schedule: { interval: '10s' }, + consumer: 'myApp', + scheduledTaskId: 'task-123', + params: {}, + throttle: null, + actions: [], + muteAll: false, + mutedInstanceIds: [], + ...attributeOverrides, + }, + references: [], + version: '123', + ...overrides, + }; + const decryptedRawAlert = { + ...rawAlert, + attributes: { + ...rawAlert.attributes, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + }; + + unsecuredSavedObjectsClient.get.mockReset(); + encryptedSavedObjects.getDecryptedAsInternalUser.mockReset(); + + // splitting this out as it's easier to set a breakpoint :-) + // eslint-disable-next-line prettier/prettier + unsecuredSavedObjectsClient.get.mockImplementation(async () => + cloneDeep(rawAlert) + ); + + encryptedSavedObjects.getDecryptedAsInternalUser.mockImplementation(async () => + cloneDeep(decryptedRawAlert) + ); +} + +// setup for each test +beforeEach(() => { + jest.resetAllMocks(); + + alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false }); + alertsClientParams.invalidateAPIKey.mockResolvedValue({ + apiKeysEnabled: true, + result: { + invalidated_api_keys: [], + previously_invalidated_api_keys: [], + error_count: 0, + }, + }); + alertsClientParams.getUserName.mockResolvedValue('elastic'); + + taskManager.runNow.mockResolvedValue({ id: '' }); + taskManager.schedule.mockResolvedValue({ + id: 'scheduled-task-id', + scheduledAt: new Date(), + attempts: 0, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + ownerId: null, + taskType: 'task-type', + params: {}, + }); + + const actionsClient = actionsClientMock.create(); + actionsClient.getBulk.mockResolvedValue([]); + alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); + + alertTypeRegistry.get.mockImplementation((id) => ({ + id: '123', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + async executor() {}, + producer: 'alerts', + })); + + alertTypeRegistry.get.mockReturnValue({ + id: 'myType', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + async executor() {}, + producer: 'alerts', + }); + + alertsClient = new AlertsClient(alertsClientParams); + + setupRawAlertMocks(); +}); diff --git a/x-pack/plugins/alerts/server/lib/retry_if_conflicts.test.ts b/x-pack/plugins/alerts/server/lib/retry_if_conflicts.test.ts new file mode 100644 index 0000000000000..19caccc753e38 --- /dev/null +++ b/x-pack/plugins/alerts/server/lib/retry_if_conflicts.test.ts @@ -0,0 +1,78 @@ +/* + * 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 { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; +import { retryIfConflicts, RetryForConflictsAttempts } from './retry_if_conflicts'; +import { loggingSystemMock } from '../../../../../src/core/server/mocks'; + +describe('retry_if_conflicts', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('should work when operation is a success', async () => { + const result = await retryIfConflicts(MockLogger, MockOperationName, OperationSuccessful); + expect(result).toBe(MockResult); + }); + + test('should throw error if not a conflict error', async () => { + await expect( + retryIfConflicts(MockLogger, MockOperationName, OperationFailure) + ).rejects.toThrowError('wops'); + }); + + for (let i = 1; i <= RetryForConflictsAttempts; i++) { + test(`should work when operation conflicts ${i} times`, async () => { + const result = await retryIfConflicts( + MockLogger, + MockOperationName, + getOperationConflictsTimes(i) + ); + expect(result).toBe(MockResult); + expect(MockLogger.debug).toBeCalledTimes(i); + for (let j = 0; j < i; j++) { + expect(MockLogger.debug).nthCalledWith(i, `${MockOperationName} conflict, retrying ...`); + } + }); + } + + test(`should throw conflict error when conflicts > ${RetryForConflictsAttempts} times`, async () => { + await expect( + retryIfConflicts( + MockLogger, + MockOperationName, + getOperationConflictsTimes(RetryForConflictsAttempts + 1) + ) + ).rejects.toThrowError(SavedObjectsErrorHelpers.createConflictError('alert', MockAlertId)); + expect(MockLogger.debug).toBeCalledTimes(RetryForConflictsAttempts); + expect(MockLogger.warn).toBeCalledTimes(1); + expect(MockLogger.warn).toBeCalledWith(`${MockOperationName} conflict, exceeded retries`); + }); +}); + +const MockAlertId = 'alert-id'; +const MockOperationName = 'conflict-retryable-operation'; +const MockLogger = loggingSystemMock.create().get(); +const MockResult = 42; + +async function OperationSuccessful() { + return MockResult; +} + +async function OperationFailure() { + throw new Error('wops'); +} + +function getOperationConflictsTimes(times: number) { + return async function OperationConflictsTimes() { + times--; + if (times >= 0) { + throw SavedObjectsErrorHelpers.createConflictError('alert', MockAlertId); + } + + return MockResult; + }; +} diff --git a/x-pack/plugins/alerts/server/lib/retry_if_conflicts.ts b/x-pack/plugins/alerts/server/lib/retry_if_conflicts.ts new file mode 100644 index 0000000000000..9cb1d7975855c --- /dev/null +++ b/x-pack/plugins/alerts/server/lib/retry_if_conflicts.ts @@ -0,0 +1,58 @@ +/* + * 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. + */ + +// This module provides a helper to perform retries on a function if the +// function ends up throwing a SavedObject 409 conflict. This can happen +// when alert SO's are updated in the background, and will avoid having to +// have the caller make explicit conflict checks, where the conflict was +// caused by a background update. + +import { Logger, SavedObjectsErrorHelpers } from '../../../../../src/core/server'; + +type RetryableForConflicts = () => Promise; + +// number of times to retry when conflicts occur +// note: it seems unlikely that we'd need more than one retry, but leaving +// this statically configurable in case we DO need > 1 +export const RetryForConflictsAttempts = 1; + +// milliseconds to wait before retrying when conflicts occur +// note: we considered making this random, to help avoid a stampede, but +// with 1 retry it probably doesn't matter, and adding randomness could +// make it harder to diagnose issues +const RetryForConflictsDelay = 250; + +// retry an operation if it runs into 409 Conflict's, up to a limit +export async function retryIfConflicts( + logger: Logger, + name: string, + operation: RetryableForConflicts, + retries: number = RetryForConflictsAttempts +): Promise { + // run the operation, return if no errors or throw if not a conflict error + try { + return await operation(); + } catch (err) { + if (!SavedObjectsErrorHelpers.isConflictError(err)) { + throw err; + } + + // must be a conflict; if no retries left, throw it + if (retries <= 0) { + logger.warn(`${name} conflict, exceeded retries`); + throw err; + } + + // delay a bit before retrying + logger.debug(`${name} conflict, retrying ...`); + await waitBeforeNextRetry(); + return await retryIfConflicts(logger, name, operation, retries - 1); + } +} + +async function waitBeforeNextRetry(): Promise { + await new Promise((resolve) => setTimeout(resolve, RetryForConflictsDelay)); +} diff --git a/x-pack/plugins/alerts/server/saved_objects/index.ts b/x-pack/plugins/alerts/server/saved_objects/index.ts index 06ce8d673e6b7..51ac68b589977 100644 --- a/x-pack/plugins/alerts/server/saved_objects/index.ts +++ b/x-pack/plugins/alerts/server/saved_objects/index.ts @@ -9,6 +9,23 @@ import mappings from './mappings.json'; import { getMigrations } from './migrations'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; +export { partiallyUpdateAlert } from './partially_update_alert'; + +export const AlertAttributesExcludedFromAAD = [ + 'scheduledTaskId', + 'muteAll', + 'mutedInstanceIds', + 'updatedBy', +]; + +// useful for Pick which is a +// type which is a subset of RawAlert with just attributes excluded from AAD +export type AlertAttributesExcludedFromAADType = + | 'scheduledTaskId' + | 'muteAll' + | 'mutedInstanceIds' + | 'updatedBy'; + export function setupSavedObjects( savedObjects: SavedObjectsServiceSetup, encryptedSavedObjects: EncryptedSavedObjectsPluginSetup diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts index 1c1261ae3fa08..10e1a9ae421b7 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts @@ -85,6 +85,120 @@ describe('7.10.0', () => { }, }); }); + + test('migrates PagerDuty actions to set a default dedupkey of the AlertId', () => { + const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; + const alert = getMockData({ + actions: [ + { + actionTypeId: '.pagerduty', + group: 'default', + params: { + summary: 'fired {{alertInstanceId}}', + eventAction: 'resolve', + component: '', + }, + id: 'b62ea790-5366-4abc-a7df-33db1db78410', + }, + ], + }); + expect(migration710(alert, { log })).toMatchObject({ + ...alert, + attributes: { + ...alert.attributes, + actions: [ + { + actionTypeId: '.pagerduty', + group: 'default', + params: { + summary: 'fired {{alertInstanceId}}', + eventAction: 'resolve', + dedupKey: '{{alertId}}', + component: '', + }, + id: 'b62ea790-5366-4abc-a7df-33db1db78410', + }, + ], + }, + }); + }); + + test('skips PagerDuty actions with a specified dedupkey', () => { + const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; + const alert = getMockData({ + actions: [ + { + actionTypeId: '.pagerduty', + group: 'default', + params: { + summary: 'fired {{alertInstanceId}}', + eventAction: 'trigger', + dedupKey: '{{alertInstanceId}}', + component: '', + }, + id: 'b62ea790-5366-4abc-a7df-33db1db78410', + }, + ], + }); + expect(migration710(alert, { log })).toMatchObject({ + ...alert, + attributes: { + ...alert.attributes, + actions: [ + { + actionTypeId: '.pagerduty', + group: 'default', + params: { + summary: 'fired {{alertInstanceId}}', + eventAction: 'trigger', + dedupKey: '{{alertInstanceId}}', + component: '', + }, + id: 'b62ea790-5366-4abc-a7df-33db1db78410', + }, + ], + }, + }); + }); + + test('skips PagerDuty actions with an eventAction of "trigger"', () => { + const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; + const alert = getMockData({ + actions: [ + { + actionTypeId: '.pagerduty', + group: 'default', + params: { + summary: 'fired {{alertInstanceId}}', + eventAction: 'trigger', + component: '', + }, + id: 'b62ea790-5366-4abc-a7df-33db1db78410', + }, + ], + }); + expect(migration710(alert, { log })).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + meta: { + versionApiKeyLastmodified: 'pre-7.10.0', + }, + actions: [ + { + actionTypeId: '.pagerduty', + group: 'default', + params: { + summary: 'fired {{alertInstanceId}}', + eventAction: 'trigger', + component: '', + }, + id: 'b62ea790-5366-4abc-a7df-33db1db78410', + }, + ], + }, + }); + }); }); describe('7.10.0 migrates with failure', () => { diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts index c88f4d786c212..537c21e85c0bd 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -18,10 +18,20 @@ import { export const LEGACY_LAST_MODIFIED_VERSION = 'pre-7.10.0'; +type AlertMigration = ( + doc: SavedObjectUnsanitizedDoc +) => SavedObjectUnsanitizedDoc; + export function getMigrations( encryptedSavedObjects: EncryptedSavedObjectsPluginSetup ): SavedObjectMigrationMap { - const migrationWhenRBACWasIntroduced = markAsLegacyAndChangeConsumer(encryptedSavedObjects); + const migrationWhenRBACWasIntroduced = encryptedSavedObjects.createMigration( + function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { + // migrate all documents in 7.10 in order to add the "meta" RBAC field + return true; + }, + pipeMigrations(markAsLegacyAndChangeConsumer, setAlertIdAsDefaultDedupkeyOnPagerDutyActions) + ); return { '7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'), @@ -52,29 +62,55 @@ const consumersToChange: Map = new Map( [SIEM_APP_ID]: SIEM_SERVER_APP_ID, }) ); + function markAsLegacyAndChangeConsumer( - encryptedSavedObjects: EncryptedSavedObjectsPluginSetup -): SavedObjectMigrationFn { - return encryptedSavedObjects.createMigration( - function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { - // migrate all documents in 7.10 in order to add the "meta" RBAC field - return true; + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + const { + attributes: { consumer }, + } = doc; + return { + ...doc, + attributes: { + ...doc.attributes, + consumer: consumersToChange.get(consumer) ?? consumer, + // mark any alert predating 7.10 as a legacy alert + meta: { + versionApiKeyLastmodified: LEGACY_LAST_MODIFIED_VERSION, + }, }, - (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { - const { - attributes: { consumer }, - } = doc; - return { - ...doc, - attributes: { - ...doc.attributes, - consumer: consumersToChange.get(consumer) ?? consumer, - // mark any alert predating 7.10 as a legacy alert - meta: { - versionApiKeyLastmodified: LEGACY_LAST_MODIFIED_VERSION, - }, - }, - }; - } - ); + }; +} + +function setAlertIdAsDefaultDedupkeyOnPagerDutyActions( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + const { attributes } = doc; + return { + ...doc, + attributes: { + ...attributes, + ...(attributes.actions + ? { + actions: attributes.actions.map((action) => { + if (action.actionTypeId !== '.pagerduty' || action.params.eventAction === 'trigger') { + return action; + } + return { + ...action, + params: { + ...action.params, + dedupKey: action.params.dedupKey ?? '{{alertId}}', + }, + }; + }), + } + : {}), + }, + }; +} + +function pipeMigrations(...migrations: AlertMigration[]): AlertMigration { + return (doc: SavedObjectUnsanitizedDoc) => + migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc); } diff --git a/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts b/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts new file mode 100644 index 0000000000000..50815c797e399 --- /dev/null +++ b/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts @@ -0,0 +1,112 @@ +/* + * 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 { + SavedObjectsClientContract, + ISavedObjectsRepository, + SavedObjectsErrorHelpers, +} from '../../../../../src/core/server'; + +import { partiallyUpdateAlert, PartiallyUpdateableAlertAttributes } from './partially_update_alert'; +import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; + +const MockSavedObjectsClientContract = savedObjectsClientMock.create(); +const MockISavedObjectsRepository = (MockSavedObjectsClientContract as unknown) as jest.Mocked< + ISavedObjectsRepository +>; + +describe('partially_update_alert', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + for (const [soClientName, soClient] of Object.entries(getMockSavedObjectClients())) + describe(`using ${soClientName}`, () => { + test('should work with no options', async () => { + soClient.update.mockResolvedValueOnce(MockUpdateValue); + + await partiallyUpdateAlert(soClient, MockAlertId, DefaultAttributes); + expect(soClient.update).toHaveBeenCalledWith('alert', MockAlertId, DefaultAttributes, {}); + }); + + test('should work with extraneous attributes ', async () => { + const attributes = (InvalidAttributes as unknown) as PartiallyUpdateableAlertAttributes; + soClient.update.mockResolvedValueOnce(MockUpdateValue); + + await partiallyUpdateAlert(soClient, MockAlertId, attributes); + expect(soClient.update).toHaveBeenCalledWith('alert', MockAlertId, DefaultAttributes, {}); + }); + + test('should handle SO errors', async () => { + soClient.update.mockRejectedValueOnce(new Error('wops')); + + await expect( + partiallyUpdateAlert(soClient, MockAlertId, DefaultAttributes) + ).rejects.toThrowError('wops'); + }); + + test('should handle the version option', async () => { + soClient.update.mockResolvedValueOnce(MockUpdateValue); + + await partiallyUpdateAlert(soClient, MockAlertId, DefaultAttributes, { version: '1.2.3' }); + expect(soClient.update).toHaveBeenCalledWith('alert', MockAlertId, DefaultAttributes, { + version: '1.2.3', + }); + }); + + test('should handle the ignore404 option', async () => { + const err = SavedObjectsErrorHelpers.createGenericNotFoundError(); + soClient.update.mockRejectedValueOnce(err); + + await partiallyUpdateAlert(soClient, MockAlertId, DefaultAttributes, { ignore404: true }); + expect(soClient.update).toHaveBeenCalledWith('alert', MockAlertId, DefaultAttributes, {}); + }); + + test('should handle the namespace option', async () => { + soClient.update.mockResolvedValueOnce(MockUpdateValue); + + await partiallyUpdateAlert(soClient, MockAlertId, DefaultAttributes, { + namespace: 'bat.cave', + }); + expect(soClient.update).toHaveBeenCalledWith('alert', MockAlertId, DefaultAttributes, { + namespace: 'bat.cave', + }); + }); + }); +}); + +function getMockSavedObjectClients(): Record< + string, + jest.Mocked +> { + return { + SavedObjectsClientContract: MockSavedObjectsClientContract, + // doesn't appear to be a mock for this, but it's basically the same as the above, + // so just cast it to make sure we catch any type errors + ISavedObjectsRepository: MockISavedObjectsRepository, + }; +} + +const DefaultAttributes = { + scheduledTaskId: 'scheduled-task-id', + muteAll: true, + mutedInstanceIds: ['muted-instance-id-1', 'muted-instance-id-2'], + updatedBy: 'someone', +}; + +const InvalidAttributes = { ...DefaultAttributes, foo: 'bar' }; + +const MockAlertId = 'alert-id'; + +const MockUpdateValue = { + id: MockAlertId, + type: 'alert', + attributes: { + actions: [], + scheduledTaskId: 'scheduled-task-id', + }, + references: [], +}; diff --git a/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.ts b/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.ts new file mode 100644 index 0000000000000..cc25aaba35798 --- /dev/null +++ b/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.ts @@ -0,0 +1,49 @@ +/* + * 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 { pick } from 'lodash'; +import { RawAlert } from '../types'; + +import { + SavedObjectsClient, + SavedObjectsErrorHelpers, + SavedObjectsUpdateOptions, +} from '../../../../../src/core/server'; + +import { AlertAttributesExcludedFromAAD, AlertAttributesExcludedFromAADType } from './index'; + +export type PartiallyUpdateableAlertAttributes = Pick; + +export interface PartiallyUpdateAlertSavedObjectOptions { + version?: string; + ignore404?: boolean; + namespace?: string; // only should be used with ISavedObjectsRepository +} + +// typed this way so we can send a SavedObjectClient or SavedObjectRepository +type SavedObjectClientForUpdate = Pick; + +// direct, partial update to an alert saved object via scoped SavedObjectsClient +// using namespace set in the client +export async function partiallyUpdateAlert( + savedObjectsClient: SavedObjectClientForUpdate, + id: string, + attributes: PartiallyUpdateableAlertAttributes, + options: PartiallyUpdateAlertSavedObjectOptions = {} +): Promise { + // ensure we only have the valid attributes excluded from AAD + const attributeUpdates = pick(attributes, AlertAttributesExcludedFromAAD); + const updateOptions: SavedObjectsUpdateOptions = pick(options, 'namespace', 'version'); + + try { + await savedObjectsClient.update('alert', id, attributeUpdates, updateOptions); + } catch (err) { + if (options?.ignore404 && SavedObjectsErrorHelpers.isNotFoundError(err)) { + return; + } + throw err; + } +} diff --git a/x-pack/plugins/apm/e2e/package.json b/x-pack/plugins/apm/e2e/package.json index 56204b5fb9f9d..649198b7eae11 100644 --- a/x-pack/plugins/apm/e2e/package.json +++ b/x-pack/plugins/apm/e2e/package.json @@ -7,23 +7,21 @@ "cypress:open": "cypress open", "cypress:run": "cypress run --spec **/*.feature" }, - "dependencies": { + "devDependencies": { "@cypress/snapshot": "^2.1.3", "@cypress/webpack-preprocessor": "^5.4.1", + "@types/cypress-cucumber-preprocessor": "^1.14.1", + "@types/node": ">=10.17.17 <10.20.0", "axios": "^0.19.2", - "cypress": "^4.9.0", "cypress-cucumber-preprocessor": "^2.5.2", + "cypress": "^4.9.0", "ora": "^4.0.4", "p-limit": "^3.0.1", "p-retry": "^4.2.0", "ts-loader": "^7.0.5", "typescript": "4.0.2", "wait-on": "^5.0.1", - "webpack": "^4.43.0", - "yargs": "^15.4.0" - }, - "devDependencies": { - "@types/cypress-cucumber-preprocessor": "^1.14.1", - "@types/node": "^14.0.14" + "webpack": "^4.41.5", + "yargs": "^15.4.1" } } diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx index 5fa74f927d2de..d65ce1879ce02 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx @@ -6,9 +6,7 @@ import cytoscape from 'cytoscape'; import dagre from 'cytoscape-dagre'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import isEqual from 'lodash/isEqual'; +import { isEqual } from 'lodash'; import React, { createContext, CSSProperties, diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/empty_prompt.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/empty_prompt.tsx new file mode 100644 index 0000000000000..128b5696b23b8 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/empty_prompt.tsx @@ -0,0 +1,35 @@ +/* + * 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 { EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink'; + +export function EmptyPrompt() { + return ( + + {i18n.translate('xpack.apm.serviceMap.noServicesPromptTitle', { + defaultMessage: 'No services available', + })} + + } + body={ +

+ {i18n.translate('xpack.apm.serviceMap.noServicesPromptDescription', { + defaultMessage: + 'We can’t find any services to map within the currently selected time range and environment. Please try another range or check the environment selected. If you don’t have any services, please use our setup instructions to get started.', + })} +

+ } + actions={} + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx index f504ac1f68ce1..2a5b4ce44ff46 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx @@ -9,16 +9,30 @@ import { CoreStart } from 'kibana/public'; import React, { ReactNode } from 'react'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; import { License } from '../../../../../licensing/common/license'; +import { EuiThemeProvider } from '../../../../../observability/public'; +import { FETCH_STATUS } from '../../../../../observability/public/hooks/use_fetcher'; import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; import { LicenseContext } from '../../../context/LicenseContext'; +import * as useFetcherModule from '../../../hooks/useFetcher'; import { ServiceMap } from './'; const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiStats: () => {} }, } as Partial); +const activeLicense = new License({ + signature: 'active test signature', + license: { + expiryDateInMillis: 0, + mode: 'platinum', + status: 'active', + type: 'platinum', + uid: '1', + }, +}); + const expiredLicense = new License({ - signature: 'test signature', + signature: 'expired test signature', license: { expiryDateInMillis: 0, mode: 'platinum', @@ -28,26 +42,58 @@ const expiredLicense = new License({ }, }); -function Wrapper({ children }: { children?: ReactNode }) { - return ( - - - {children} - - - ); +function createWrapper(license: License | null) { + return ({ children }: { children?: ReactNode }) => { + return ( + + + + + {children} + + + + + ); + }; } describe('ServiceMap', () => { - describe('with an inactive license', () => { + describe('with no license', () => { + it('renders null', async () => { + expect( + await render(, { + wrapper: createWrapper(null), + }).queryByTestId('ServiceMap') + ).not.toBeInTheDocument(); + }); + }); + + describe('with an expired license', () => { it('renders the license banner', async () => { expect( - ( + await render(, { + wrapper: createWrapper(expiredLicense), + }).findAllByText(/Platinum/) + ).toHaveLength(1); + }); + }); + + describe('with an active license', () => { + describe('with an empty response', () => { + it('renders the empty banner', async () => { + jest.spyOn(useFetcherModule, 'useFetcher').mockReturnValueOnce({ + data: { elements: [] }, + refetch: () => {}, + status: FETCH_STATUS.SUCCESS, + }); + + expect( await render(, { - wrapper: Wrapper, - }).findAllByText(/Platinum/) - ).length - ).toBeGreaterThan(0); + wrapper: createWrapper(activeLicense), + }).findAllByText(/No services available/) + ).toHaveLength(1); + }); }); }); }); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index bb450131bdfb8..1d2e4ada43add 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import React, { ReactNode } from 'react'; import { useTrackPageview } from '../../../../../observability/public'; import { invalidLicenseMessage, isActivePlatinumLicense, } from '../../../../common/service_map'; -import { useFetcher } from '../../../hooks/useFetcher'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; import { useLicense } from '../../../hooks/useLicense'; import { useTheme } from '../../../hooks/useTheme'; import { useUrlParams } from '../../../hooks/useUrlParams'; @@ -21,6 +21,7 @@ import { Controls } from './Controls'; import { Cytoscape } from './Cytoscape'; import { getCytoscapeDivStyle } from './cytoscapeOptions'; import { EmptyBanner } from './EmptyBanner'; +import { EmptyPrompt } from './empty_prompt'; import { Popover } from './Popover'; import { useRefDimensions } from './useRefDimensions'; @@ -28,12 +29,39 @@ interface ServiceMapProps { serviceName?: string; } +function PromptContainer({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} + +function LoadingSpinner() { + return ( + + ); +} + export function ServiceMap({ serviceName }: ServiceMapProps) { const theme = useTheme(); const license = useLicense(); const { urlParams } = useUrlParams(); - const { data = { elements: [] } } = useFetcher(() => { + const { data = { elements: [] }, status } = useFetcher(() => { // When we don't have a license or a valid license, don't make the request. if (!license || !isActivePlatinumLicense(license)) { return; @@ -65,37 +93,41 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { return null; } - return isActivePlatinumLicense(license) ? ( + if (!isActivePlatinumLicense(license)) { + return ( + + + + ); + } + + if (status === FETCH_STATUS.SUCCESS && data.elements.length === 0) { + return ( + + + + ); + } + + return (
{serviceName && } + {status === FETCH_STATUS.LOADING && }
- ) : ( - - - - - ); } diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/use_cytoscape_event_handlers.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/use_cytoscape_event_handlers.ts index e77fc4dfb2f51..e8c6a3165ce93 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/use_cytoscape_event_handlers.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/use_cytoscape_event_handlers.ts @@ -5,9 +5,7 @@ */ import cytoscape from 'cytoscape'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import debounce from 'lodash/debounce'; +import { debounce } from 'lodash'; import { useEffect } from 'react'; import { EuiTheme, useUiTracker } from '../../../../../observability/public'; import { getAnimationOptions, getNodeHeight } from './cytoscapeOptions'; diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts index af2fc85602541..6e34e4c1964c5 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts @@ -8,6 +8,7 @@ import { coreMock } from '../../../../../src/core/public/mocks'; import { EnhancedSearchInterceptor } from './search_interceptor'; import { CoreSetup, CoreStart } from 'kibana/public'; import { AbortError, UI_SETTINGS } from '../../../../../src/plugins/data/common'; +import { SearchTimeoutError } from 'src/plugins/data/public'; const timeTravel = (msToRun = 0) => { jest.advanceTimersByTime(msToRun); @@ -265,7 +266,7 @@ describe('EnhancedSearchInterceptor', () => { await timeTravel(1000); expect(error).toHaveBeenCalled(); - expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError); + expect(error.mock.calls[0][0]).toBeInstanceOf(SearchTimeoutError); expect(mockCoreSetup.http.fetch).toHaveBeenCalled(); expect(mockCoreSetup.http.delete).not.toHaveBeenCalled(); }); @@ -305,7 +306,7 @@ describe('EnhancedSearchInterceptor', () => { await timeTravel(1000); expect(error).toHaveBeenCalled(); - expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError); + expect(error.mock.calls[0][0]).toBeInstanceOf(SearchTimeoutError); expect(mockCoreSetup.http.fetch).toHaveBeenCalledTimes(2); expect(mockCoreSetup.http.delete).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index c8fe72e6f2c1e..cca87c85e326c 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -5,9 +5,7 @@ */ import { throwError, EMPTY, timer, from, Subscription } from 'rxjs'; -import { mergeMap, expand, takeUntil, finalize, tap } from 'rxjs/operators'; -import { debounce } from 'lodash'; -import { i18n } from '@kbn/i18n'; +import { mergeMap, expand, takeUntil, finalize, catchError } from 'rxjs/operators'; import { SearchInterceptor, SearchInterceptorDeps, @@ -15,6 +13,7 @@ import { } from '../../../../../src/plugins/data/public'; import { isErrorResponse, isCompleteResponse } from '../../../../../src/plugins/data/public'; import { AbortError, toPromise } from '../../../../../src/plugins/data/common'; +import { TimeoutErrorMode } from '../../../../../src/plugins/data/public'; import { IAsyncSearchOptions } from '.'; import { IAsyncSearchRequest, ENHANCED_ES_SEARCH_STRATEGY } from '../../common'; @@ -40,6 +39,12 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { this.uiSettingsSub.unsubscribe(); } + protected getTimeoutMode() { + return this.application.capabilities.advancedSettings?.save + ? TimeoutErrorMode.CHANGE + : TimeoutErrorMode.CONTACT; + } + /** * Abort our `AbortController`, which in turn aborts any intercepted searches. */ @@ -55,7 +60,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { ) { let { id } = request; - const { combinedSignal, cleanup } = this.setupAbortSignal({ + const { combinedSignal, timeoutSignal, cleanup } = this.setupAbortSignal({ abortSignal: options.abortSignal, timeout: this.searchTimeout, }); @@ -86,15 +91,14 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { ); }), takeUntil(aborted$), - tap({ - error: () => { - // If we haven't received the response to the initial request, including the ID, then - // we don't need to send a follow-up request to delete this search. Otherwise, we - // send the follow-up request to delete this search, then throw an abort error. - if (id !== undefined) { - this.deps.http.delete(`/internal/search/${strategy}/${id}`); - } - }, + catchError((e: any) => { + // If we haven't received the response to the initial request, including the ID, then + // we don't need to send a follow-up request to delete this search. Otherwise, we + // send the follow-up request to delete this search, then throw an abort error. + if (id !== undefined) { + this.deps.http.delete(`/internal/search/${strategy}/${id}`); + } + return throwError(this.handleSearchError(e, request, timeoutSignal, options?.abortSignal)); }), finalize(() => { this.pendingCount$.next(this.pendingCount$.getValue() - 1); @@ -102,28 +106,4 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { }) ); } - - // Right now we are debouncing but we will hook this up with background sessions to show only one - // error notification per session. - protected showTimeoutError = debounce( - (e: Error) => { - const message = this.application.capabilities.advancedSettings?.save - ? i18n.translate('xpack.data.search.timeoutIncreaseSetting', { - defaultMessage: - 'One or more queries timed out. Increase run time with the search.timeout advanced setting.', - }) - : i18n.translate('xpack.data.search.timeoutContactAdmin', { - defaultMessage: - 'One or more queries timed out. Contact your system administrator to increase the run time.', - }); - this.deps.toasts.addError(e, { - title: 'Timed out', - toastMessage: message, - }); - }, - 60000, - { - leading: true, - } - ); } diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts index 88a900f69c5ec..f48f5fb91e3e7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts @@ -5,17 +5,13 @@ */ export { mockHistory, mockLocation } from './react_router_history.mock'; -export { mockKibanaContext } from './kibana_context.mock'; +export { mockKibanaValues } from './kibana_logic.mock'; export { mockLicensingValues } from './licensing_logic.mock'; export { mockHttpValues } from './http_logic.mock'; export { mockFlashMessagesValues, mockFlashMessagesActions } from './flash_messages_logic.mock'; export { mockAllValues, mockAllActions, setMockValues } from './kea.mock'; -export { - mountWithContext, - mountWithKibanaContext, - mountWithAsyncContext, -} from './mount_with_context.mock'; +export { mountAsync } from './mount_async.mock'; +export { mountWithIntl } from './mount_with_i18n.mock'; export { shallowWithIntl } from './shallow_with_i18n.mock'; - -// Note: shallow_usecontext must be imported directly as a file +// Note: shallow_useeffect must be imported directly as a file diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts index bad6beaa1652e..b616cbab03e28 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts @@ -10,11 +10,13 @@ * NOTE: These variable names MUST start with 'mock*' in order for * Jest to accept its use within a jest.mock() */ +import { mockKibanaValues } from './kibana_logic.mock'; import { mockLicensingValues } from './licensing_logic.mock'; import { mockHttpValues } from './http_logic.mock'; import { mockFlashMessagesValues, mockFlashMessagesActions } from './flash_messages_logic.mock'; export const mockAllValues = { + ...mockKibanaValues, ...mockLicensingValues, ...mockHttpValues, ...mockFlashMessagesValues, diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts similarity index 65% rename from x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts rename to x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts index ee77b0937cd82..9f3c2443bc9b8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts @@ -4,13 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -/** - * A set of default Kibana context values to use across component tests. - * @see enterprise_search/public/index.tsx for the KibanaContext definition/import - */ -export const mockKibanaContext = { +export const mockKibanaValues = { + config: { host: 'http://localhost:3002' }, navigateToUrl: jest.fn(), setBreadcrumbs: jest.fn(), setDocTitle: jest.fn(), - config: { host: 'http://localhost:3002' }, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx new file mode 100644 index 0000000000000..a33e116c7ca72 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx @@ -0,0 +1,46 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { mount, ReactWrapper } from 'enzyme'; + +import { mountWithIntl } from './'; + +/** + * This helper is intended for components that have async effects + * (e.g. http fetches) on mount. It mostly adds act/update boilerplate + * that's needed for the wrapper to play nice with Enzyme/Jest + * + * Example usage: + * + * const wrapper = mountAsync(); + */ + +interface IOptions { + i18n?: boolean; +} + +export const mountAsync = async ( + children: React.ReactElement, + options: IOptions +): Promise => { + let wrapper: ReactWrapper | undefined; + + // We get a lot of act() warning/errors in the terminal without this. + // TBH, I don't fully understand why since Enzyme's mount is supposed to + // have act() baked in - could be because of the wrapping context provider? + await act(async () => { + wrapper = options.i18n ? mountWithIntl(children) : mount(children); + }); + if (wrapper) { + wrapper.update(); // This seems to be required for the DOM to actually update + + return wrapper; + } else { + throw new Error('Could not mount wrapper'); + } +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx deleted file mode 100644 index 646c3104c286f..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx +++ /dev/null @@ -1,83 +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 { act } from 'react-dom/test-utils'; -import { mount, ReactWrapper } from 'enzyme'; - -import { Provider } from 'react-redux'; -import { Store } from 'redux'; -import { getContext, resetContext } from 'kea'; - -import { I18nProvider } from '@kbn/i18n/react'; -import { KibanaContext } from '../'; -import { mockKibanaContext } from './kibana_context.mock'; - -/** - * This helper mounts a component with all the contexts/providers used - * by the production app, while allowing custom context to be - * passed in via a second arg - * - * Example usage: - * - * const wrapper = mountWithContext(, { config: { host: 'someOverride' } }); - */ -export const mountWithContext = (children: React.ReactNode, context?: object) => { - resetContext({ createStore: true }); - const store = getContext().store as Store; - - return mount( - - - {children} - - - ); -}; - -/** - * This helper mounts a component with just the default KibanaContext - - * useful for isolated / helper components that only need this context - * - * Same usage/override functionality as mountWithContext - */ -export const mountWithKibanaContext = (children: React.ReactNode, context?: object) => { - return mount( - - {children} - - ); -}; - -/** - * This helper is intended for components that have async effects - * (e.g. http fetches) on mount. It mostly adds act/update boilerplate - * that's needed for the wrapper to play nice with Enzyme/Jest - * - * Example usage: - * - * const wrapper = mountWithAsyncContext(, { http: { get: () => someData } }); - */ -export const mountWithAsyncContext = async ( - children: React.ReactNode, - context?: object -): Promise => { - let wrapper: ReactWrapper | undefined; - - // We get a lot of act() warning/errors in the terminal without this. - // TBH, I don't fully understand why since Enzyme's mount is supposed to - // have act() baked in - could be because of the wrapping context provider? - await act(async () => { - wrapper = mountWithContext(children, context); - }); - if (wrapper) { - wrapper.update(); // This seems to be required for the DOM to actually update - - return wrapper; - } else { - throw new Error('Could not mount wrapper'); - } -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_i18n.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_i18n.mock.tsx new file mode 100644 index 0000000000000..55abe1030544f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_i18n.mock.tsx @@ -0,0 +1,21 @@ +/* + * 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 { mount } from 'enzyme'; +import { I18nProvider } from '@kbn/i18n/react'; + +/** + * This helper wraps a component with react-intl's which + * fixes "Could not find required `intl` object" console errors when running tests + * + * Example usage (should be the same as mount()): + * + * const wrapper = mountWithI18n(); + */ +export const mountWithIntl = (children: React.ReactElement) => { + return mount({children}); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts deleted file mode 100644 index df9e58994e36b..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts +++ /dev/null @@ -1,40 +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. - */ - -/** - * NOTE: These variable names MUST start with 'mock*' in order for - * Jest to accept its use within a jest.mock() - */ -import { mockKibanaContext } from './kibana_context.mock'; - -jest.mock('react', () => ({ - ...(jest.requireActual('react') as object), - useContext: jest.fn(() => ({ ...mockKibanaContext })), - useEffect: jest.fn((fn) => fn()), // Calls on mount/every update - use mount for more complex behavior -})); - -/** - * Example usage within a component test using shallow(): - * - * import '../../../__mocks__/shallow_usecontext'; // Must come before React's import, adjust relative path as needed - * - * import React from 'react'; - * import { shallow } from 'enzyme'; - * - * // ... etc. - */ - -/** - * If you need to override the default mock context values, you can do so via jest.mockImplementation: - * - * import React, { useContext } from 'react'; - * - * // ... etc. - * - * it('some test', () => { - * useContext.mockImplementationOnce(() => ({ config: { host: 'someOverride' } })); - * }); - */ diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_useeffect.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_useeffect.mock.ts new file mode 100644 index 0000000000000..732786b5f9249 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_useeffect.mock.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +jest.mock('react', () => ({ + ...(jest.requireActual('react') as object), + useEffect: jest.fn((fn) => fn()), // Calls on mount/every update - use mount for more complex behavior +})); + +/** + * Example usage within a component test using shallow(): + * + * import '../../../__mocks__/shallow_useeffect.mock'; // Must come before React's import, adjust relative path as needed + * + * import React from 'react'; + * import { shallow } from 'enzyme'; + * + * // ... etc. + */ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx index 44afce96c1a6c..f87ea2d422780 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { shallow, ReactWrapper } from 'enzyme'; -import { mountWithAsyncContext, mockHttpValues, setMockValues } from '../../../__mocks__'; +import { mountAsync, mockHttpValues, setMockValues } from '../../../__mocks__'; import { LoadingState, EmptyState } from './components'; import { EngineTable } from './engine_table'; @@ -36,7 +36,7 @@ describe('EngineOverview', () => { }), }, }); - const wrapper = await mountWithAsyncContext(); + const wrapper = await mountAsync(, { i18n: true }); expect(wrapper.find(EmptyState)).toHaveLength(1); }); @@ -69,7 +69,7 @@ describe('EngineOverview', () => { }); it('renders and calls the engines API', async () => { - const wrapper = await mountWithAsyncContext(); + const wrapper = await mountAsync(, { i18n: true }); expect(wrapper.find(EngineTable)).toHaveLength(1); expect(mockApi).toHaveBeenNthCalledWith(1, '/api/app_search/engines', { @@ -86,7 +86,7 @@ describe('EngineOverview', () => { hasPlatinumLicense: true, http: { ...mockHttpValues.http, get: mockApi }, }); - const wrapper = await mountWithAsyncContext(); + const wrapper = await mountAsync(, { i18n: true }); expect(wrapper.find(EngineTable)).toHaveLength(2); expect(mockApi).toHaveBeenNthCalledWith(2, '/api/app_search/engines', { @@ -103,7 +103,7 @@ describe('EngineOverview', () => { wrapper.find(EngineTable).prop('pagination'); it('passes down page data from the API', async () => { - const wrapper = await mountWithAsyncContext(); + const wrapper = await mountAsync(, { i18n: true }); const pagination = getTablePagination(wrapper); expect(pagination.totalEngines).toEqual(100); @@ -111,7 +111,7 @@ describe('EngineOverview', () => { }); it('re-polls the API on page change', async () => { - const wrapper = await mountWithAsyncContext(); + const wrapper = await mountAsync(, { i18n: true }); await act(async () => getTablePagination(wrapper).onPaginate(5)); wrapper.update(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx index c66fd24fee12a..4d97a16991b71 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx @@ -6,11 +6,9 @@ import '../../../__mocks__/kea.mock'; import '../../../__mocks__/enterprise_search_url.mock'; -import { mockHttpValues } from '../../../__mocks__/'; +import { mockHttpValues, mountWithIntl } from '../../../__mocks__/'; import React from 'react'; -import { mount } from 'enzyme'; -import { I18nProvider } from '@kbn/i18n/react'; import { EuiBasicTable, EuiPagination, EuiButtonEmpty, EuiLink } from '@elastic/eui'; jest.mock('../../../shared/telemetry', () => ({ sendTelemetry: jest.fn() })); @@ -21,24 +19,22 @@ import { EngineTable } from './engine_table'; describe('EngineTable', () => { const onPaginate = jest.fn(); // onPaginate updates the engines API call upstream - const wrapper = mount( - - - + const wrapper = mountWithIntl( + ); const table = wrapper.find(EuiBasicTable); @@ -78,13 +74,8 @@ describe('EngineTable', () => { }); it('handles empty data', () => { - const emptyWrapper = mount( - - {} }} - /> - + const emptyWrapper = mountWithIntl( + {} }} /> ); const emptyTable = emptyWrapper.find(EuiBasicTable); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 052f4446e4409..c54d6ed3ddd6f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../__mocks__/shallow_usecontext.mock'; +import '../__mocks__/shallow_useeffect.mock'; import '../__mocks__/kea.mock'; import '../__mocks__/enterprise_search_url.mock'; -import React, { useContext } from 'react'; +import React from 'react'; import { Redirect } from 'react-router-dom'; import { shallow } from 'enzyme'; import { useValues, useActions } from 'kea'; @@ -21,14 +21,14 @@ import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } f describe('AppSearch', () => { it('renders AppSearchUnconfigured when config.host is not set', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); + (useValues as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); const wrapper = shallow(); expect(wrapper.find(AppSearchUnconfigured)).toHaveLength(1); }); it('renders AppSearchConfigured when config.host set', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'some.url' } })); + (useValues as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'some.url' } })); const wrapper = shallow(); expect(wrapper.find(AppSearchConfigured)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 410f6eb524822..9aa2cce9c74df 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { Route, Redirect, Switch } from 'react-router-dom'; import { useActions, useValues } from 'kea'; import { i18n } from '@kbn/i18n'; -import { KibanaContext, IKibanaContext } from '../index'; import { getAppSearchUrl } from '../shared/enterprise_search_url'; +import { KibanaLogic } from '../shared/kibana'; import { HttpLogic } from '../shared/http'; import { AppLogic } from './app_logic'; import { IInitialAppData } from '../../../common/types'; @@ -34,7 +34,7 @@ import { NotFound } from '../shared/not_found'; import { EngineOverview } from './components/engine_overview'; export const AppSearch: React.FC = (props) => { - const { config } = useContext(KibanaContext) as IKibanaContext; + const { config } = useValues(KibanaLogic); return !config.host ? : ; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx index 35301af44b413..b2030ec910cd8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx @@ -5,9 +5,9 @@ */ import '../../../__mocks__/kea.mock'; -import '../../../__mocks__/shallow_usecontext.mock'; -import React, { useContext } from 'react'; +import React from 'react'; +import { useValues } from 'kea'; import { shallow } from 'enzyme'; import { EuiCard } from '@elastic/eui'; @@ -27,7 +27,6 @@ describe('ProductCard', () => { }); it('renders an App Search card', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'localhost' } })); const wrapper = shallow(); const card = wrapper.find(EuiCard).dive().shallow(); @@ -43,7 +42,6 @@ describe('ProductCard', () => { }); it('renders a Workplace Search card', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'localhost' } })); const wrapper = shallow(); const card = wrapper.find(EuiCard).dive().shallow(); @@ -61,7 +59,7 @@ describe('ProductCard', () => { }); it('renders correct button text when host not present', () => { - (useContext as jest.Mock).mockImplementation(() => ({ config: { host: '' } })); + (useValues as jest.Mock).mockImplementation(() => ({ config: { host: '' } })); const wrapper = shallow(); const card = wrapper.find(EuiCard).dive().shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx index 482d68736af01..1d05128adc2e3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx @@ -4,17 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; import { useValues } from 'kea'; import { snakeCase } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiCard, EuiTextColor } from '@elastic/eui'; -import { KibanaContext, IKibanaContext } from '../../../index'; - import { EuiButton } from '../../../shared/react_router_helpers'; import { sendTelemetry } from '../../../shared/telemetry'; import { HttpLogic } from '../../../shared/http'; +import { KibanaLogic } from '../../../shared/kibana'; import './product_card.scss'; @@ -31,9 +30,7 @@ interface IProductCard { export const ProductCard: React.FC = ({ product, image }) => { const { http } = useValues(HttpLogic); - const { - config: { host }, - } = useContext(KibanaContext) as IKibanaContext; + const { config } = useValues(KibanaLogic); const LAUNCH_BUTTON_TEXT = i18n.translate( 'xpack.enterpriseSearch.overview.productCard.launchButton', @@ -80,7 +77,7 @@ export const ProductCard: React.FC = ({ product, image }) => { }) } > - {host ? LAUNCH_BUTTON_TEXT : SETUP_BUTTON_TEXT} + {config.host ? LAUNCH_BUTTON_TEXT : SETUP_BUTTON_TEXT} } /> diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx index 44efa57db897f..f1f16d1a6f7a4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../../__mocks__/shallow_usecontext.mock'; +import '../../../__mocks__/kea.mock'; -import React, { useContext } from 'react'; +import React from 'react'; +import { useValues } from 'kea'; import { shallow } from 'enzyme'; import { EuiPage } from '@elastic/eui'; @@ -15,7 +16,7 @@ import { ProductCard } from '../product_card'; describe('ProductSelector', () => { it('renders the overview page and product cards with no host set', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); + (useValues as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); const wrapper = shallow(); expect(wrapper.find(EuiPage).hasClass('enterpriseSearchOverview')).toBe(true); @@ -24,7 +25,7 @@ describe('ProductSelector', () => { describe('access checks when host is set', () => { beforeEach(() => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'localhost' } })); + (useValues as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'localhost' } })); }); it('does not render the App Search card if the user does not have access to AS', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx index 07b8d4b9926d7..5c2d105e69c40 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx @@ -9,8 +9,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; - +import React from 'react'; +import { useValues } from 'kea'; import { EuiPage, EuiPageBody, @@ -24,10 +24,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { KibanaContext, IKibanaContext } from '../../../index'; - import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; - +import { KibanaLogic } from '../../../shared/kibana'; import { SetEnterpriseSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; @@ -45,12 +43,11 @@ interface IProductSelectorProps { export const ProductSelector: React.FC = ({ access }) => { const { hasAppSearchAccess, hasWorkplaceSearchAccess } = access; - const { - config: { host }, - } = useContext(KibanaContext) as IKibanaContext; + const { config } = useValues(KibanaLogic); - const shouldShowAppSearchCard = !host || hasAppSearchAccess; - const shouldShowWorkplaceSearchCard = !host || hasWorkplaceSearchAccess; + // If Enterprise Search hasn't been set up yet, show all products. Otherwise, only show products the user has access to + const shouldShowAppSearchCard = !config.host || hasAppSearchAccess; + const shouldShowWorkplaceSearchCard = !config.host || hasWorkplaceSearchAccess; return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx index 2c0902163e3d6..803d2c8462b1b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../__mocks__/shallow_usecontext.mock'; - -import React, { useContext } from 'react'; +import React from 'react'; import { shallow } from 'enzyme'; import { EuiPage } from '@elastic/eui'; @@ -19,12 +17,11 @@ import { ErrorConnecting } from './components/error_connecting'; import { ProductSelector } from './components/product_selector'; describe('EnterpriseSearch', () => { - beforeEach(() => { - (useValues as jest.Mock).mockReturnValue({ errorConnecting: false }); - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'localhost' } })); - }); - it('renders the Setup Guide and Product Selector', () => { + (useValues as jest.Mock).mockReturnValue({ + errorConnecting: false, + config: { host: 'localhost' }, + }); const wrapper = shallow(); expect(wrapper.find(SetupGuide)).toHaveLength(1); @@ -32,9 +29,10 @@ describe('EnterpriseSearch', () => { }); it('renders the error connecting prompt when host is not configured', () => { - (useValues as jest.Mock).mockReturnValueOnce({ errorConnecting: true }); - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); - + (useValues as jest.Mock).mockReturnValueOnce({ + errorConnecting: true, + config: { host: '' }, + }); const wrapper = shallow(); expect(wrapper.find(ErrorConnecting)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx index e2c05434dd0bb..7b97c6c9e58b6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; import { Route, Switch } from 'react-router-dom'; import { useValues } from 'kea'; -import { KibanaContext, IKibanaContext } from '../index'; +import { KibanaLogic } from '../shared/kibana'; import { IInitialAppData } from '../../../common/types'; import { HttpLogic } from '../shared/http'; @@ -23,7 +23,7 @@ import './index.scss'; export const EnterpriseSearch: React.FC = ({ access = {} }) => { const { errorConnecting } = useValues(HttpLogic); - const { config } = useContext(KibanaContext) as IKibanaContext; + const { config } = useValues(KibanaLogic); const showErrorConnecting = config.host && errorConnecting; diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 2c6bc787923e3..63be9b684e56f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -7,28 +7,20 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Router } from 'react-router-dom'; - import { Provider } from 'react-redux'; import { Store } from 'redux'; import { getContext, resetContext } from 'kea'; - import { I18nProvider } from '@kbn/i18n/react'; -import { AppMountParameters, CoreStart, ApplicationStart, ChromeBreadcrumb } from 'src/core/public'; + +import { AppMountParameters, CoreStart } from 'src/core/public'; import { PluginsStart, ClientConfigType, ClientData } from '../plugin'; +import { IInitialAppData } from '../../common/types'; + +import { mountKibanaLogic } from './shared/kibana'; import { mountLicensingLogic } from './shared/licensing'; import { mountHttpLogic } from './shared/http'; import { mountFlashMessagesLogic } from './shared/flash_messages'; import { externalUrl } from './shared/enterprise_search_url'; -import { IInitialAppData } from '../../common/types'; - -export interface IKibanaContext { - config: { host?: string }; - navigateToUrl: ApplicationStart['navigateToUrl']; - setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void; - setDocTitle(title: string): void; -} - -export const KibanaContext = React.createContext({}); /** * This file serves as a reusable wrapper to share Kibana-level context and other helpers @@ -47,39 +39,37 @@ export const renderApp = ( resetContext({ createStore: true }); const store = getContext().store as Store; + const unmountKibanaLogic = mountKibanaLogic({ + config, + navigateToUrl: core.application.navigateToUrl, + setBreadcrumbs: core.chrome.setBreadcrumbs, + setDocTitle: core.chrome.docTitle.change, + }); const unmountLicensingLogic = mountLicensingLogic({ license$: plugins.licensing.license$, }); - const unmountHttpLogic = mountHttpLogic({ http: core.http, errorConnecting, readOnlyMode: initialData.readOnlyMode, }); - - const unmountFlashMessagesLogic = mountFlashMessagesLogic({ history: params.history }); + const unmountFlashMessagesLogic = mountFlashMessagesLogic({ + history: params.history, + }); ReactDOM.render( - - - - - - - + + + + + , params.element ); return () => { ReactDOM.unmountComponentAtNode(params.element); + unmountKibanaLogic(); unmountLicensingLogic(); unmountHttpLogic(); unmountFlashMessagesLogic(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx index 29b773b80158a..25a02e847ccbd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../__mocks__/shallow_usecontext.mock'; +import '../../__mocks__/kea.mock'; import React from 'react'; import { shallow } from 'enzyme'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx index a2cb424dadee8..b92a5bbf1c64e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx @@ -4,17 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; +import { useValues } from 'kea'; import { EuiEmptyPrompt, EuiCode } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton } from '../react_router_helpers'; -import { KibanaContext, IKibanaContext } from '../../index'; +import { KibanaLogic } from '../../shared/kibana'; import './error_state_prompt.scss'; export const ErrorStatePrompt: React.FC = () => { - const { config } = useContext(KibanaContext) as IKibanaContext; + const { config } = useValues(KibanaLogic); return ( { + beforeEach(() => { + jest.clearAllMocks(); + resetContext({}); + }); + + describe('mounts', () => { + it('sets values from props', () => { + mountKibanaLogic(mockKibanaValues); + + expect(KibanaLogic.values).toEqual(mockKibanaValues); + }); + + it('gracefully handles missing configs', () => { + mountKibanaLogic({ ...mockKibanaValues, config: undefined } as any); + + expect(KibanaLogic.values.config).toEqual({}); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts new file mode 100644 index 0000000000000..a884acb02d10a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts @@ -0,0 +1,32 @@ +/* + * 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 { kea, MakeLogicType } from 'kea'; + +import { ApplicationStart, ChromeBreadcrumb } from 'src/core/public'; + +export interface IKibanaValues { + config: { host?: string }; + navigateToUrl: ApplicationStart['navigateToUrl']; + setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void; + setDocTitle(title: string): void; +} + +export const KibanaLogic = kea>({ + path: ['enterprise_search', 'kibana_logic'], + reducers: ({ props }) => ({ + config: [props.config || {}, {}], + navigateToUrl: [props.navigateToUrl, {}], + setBreadcrumbs: [props.setBreadcrumbs, {}], + setDocTitle: [props.setDocTitle, {}], + }), +}); + +export const mountKibanaLogic = (props: IKibanaValues) => { + KibanaLogic(props); + const unmount = KibanaLogic.mount(); + return unmount; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts index 3c8b3a7218862..a2c0bcae6fc18 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../__mocks__/shallow_usecontext.mock'; +import '../../__mocks__/kea.mock'; import '../../__mocks__/react_router_history.mock'; -import { mockKibanaContext, mockHistory } from '../../__mocks__'; +import { mockKibanaValues, mockHistory } from '../../__mocks__'; jest.mock('../react_router_helpers', () => ({ letBrowserHandleEvent: jest.fn(() => false) })); import { letBrowserHandleEvent } from '../react_router_helpers'; @@ -53,7 +53,7 @@ describe('useBreadcrumbs', () => { const event = { preventDefault: jest.fn() }; breadcrumb.onClick(event); - expect(mockKibanaContext.navigateToUrl).toHaveBeenCalledWith('/app/enterprise_search/test'); + expect(mockKibanaValues.navigateToUrl).toHaveBeenCalledWith('/app/enterprise_search/test'); expect(mockHistory.createHref).toHaveBeenCalled(); expect(event.preventDefault).toHaveBeenCalled(); }); @@ -64,7 +64,7 @@ describe('useBreadcrumbs', () => { ])[0] as any; breadcrumb.onClick({ preventDefault: () => null }); - expect(mockKibanaContext.navigateToUrl).toHaveBeenCalledWith('/test'); + expect(mockKibanaValues.navigateToUrl).toHaveBeenCalledWith('/test'); expect(mockHistory.createHref).not.toHaveBeenCalled(); }); @@ -74,7 +74,7 @@ describe('useBreadcrumbs', () => { (letBrowserHandleEvent as jest.Mock).mockImplementationOnce(() => true); breadcrumb.onClick(); - expect(mockKibanaContext.navigateToUrl).not.toHaveBeenCalled(); + expect(mockKibanaValues.navigateToUrl).not.toHaveBeenCalled(); }); it('does not generate link behavior if path is excluded', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts index 19714608e73e9..ff7f29e2e393c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useContext } from 'react'; +import { useValues } from 'kea'; import { useHistory } from 'react-router-dom'; import { EuiBreadcrumb } from '@elastic/eui'; -import { KibanaContext, IKibanaContext } from '../../index'; +import { KibanaLogic } from '../../shared/kibana'; import { ENTERPRISE_SEARCH_PLUGIN, @@ -34,7 +34,7 @@ export type TBreadcrumbs = IBreadcrumb[]; export const useBreadcrumbs = (breadcrumbs: TBreadcrumbs) => { const history = useHistory(); - const { navigateToUrl } = useContext(KibanaContext) as IKibanaContext; + const { navigateToUrl } = useValues(KibanaLogic); return breadcrumbs.map(({ text, path, shouldNotCreateHref }) => { const breadcrumb = { text } as EuiBreadcrumb; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx index 61a066bb92216..2aee224304f89 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../__mocks__/shallow_usecontext.mock'; +import '../../__mocks__/kea.mock'; +import '../../__mocks__/shallow_useeffect.mock'; import '../../__mocks__/react_router_history.mock'; +import { mockKibanaValues } from '../../__mocks__'; import React from 'react'; - -import { mockKibanaContext, mountWithKibanaContext } from '../../__mocks__'; +import { shallow } from 'enzyme'; jest.mock('./generate_breadcrumbs', () => ({ useEnterpriseSearchBreadcrumbs: jest.fn(() => (crumbs: any) => crumbs), @@ -37,13 +38,13 @@ describe('Set Kibana Chrome helpers', () => { }); afterEach(() => { - expect(mockKibanaContext.setBreadcrumbs).toHaveBeenCalled(); - expect(mockKibanaContext.setDocTitle).toHaveBeenCalled(); + expect(mockKibanaValues.setBreadcrumbs).toHaveBeenCalled(); + expect(mockKibanaValues.setDocTitle).toHaveBeenCalled(); }); describe('SetEnterpriseSearchChrome', () => { it('sets breadcrumbs and document title', () => { - mountWithKibanaContext(); + shallow(); expect(enterpriseSearchTitle).toHaveBeenCalledWith(['Hello World']); expect(useEnterpriseSearchBreadcrumbs).toHaveBeenCalledWith([ @@ -55,7 +56,7 @@ describe('Set Kibana Chrome helpers', () => { }); it('sets empty breadcrumbs and document title when isRoot is true', () => { - mountWithKibanaContext(); + shallow(); expect(enterpriseSearchTitle).toHaveBeenCalledWith([]); expect(useEnterpriseSearchBreadcrumbs).toHaveBeenCalledWith([]); @@ -64,7 +65,7 @@ describe('Set Kibana Chrome helpers', () => { describe('SetAppSearchChrome', () => { it('sets breadcrumbs and document title', () => { - mountWithKibanaContext(); + shallow(); expect(appSearchTitle).toHaveBeenCalledWith(['Engines']); expect(useAppSearchBreadcrumbs).toHaveBeenCalledWith([ @@ -76,7 +77,7 @@ describe('Set Kibana Chrome helpers', () => { }); it('sets empty breadcrumbs and document title when isRoot is true', () => { - mountWithKibanaContext(); + shallow(); expect(appSearchTitle).toHaveBeenCalledWith([]); expect(useAppSearchBreadcrumbs).toHaveBeenCalledWith([]); @@ -85,7 +86,7 @@ describe('Set Kibana Chrome helpers', () => { describe('SetWorkplaceSearchChrome', () => { it('sets breadcrumbs and document title', () => { - mountWithKibanaContext(); + shallow(); expect(workplaceSearchTitle).toHaveBeenCalledWith(['Sources']); expect(useWorkplaceSearchBreadcrumbs).toHaveBeenCalledWith([ @@ -97,7 +98,7 @@ describe('Set Kibana Chrome helpers', () => { }); it('sets empty breadcrumbs and document title when isRoot is true', () => { - mountWithKibanaContext(); + shallow(); expect(workplaceSearchTitle).toHaveBeenCalledWith([]); expect(useWorkplaceSearchBreadcrumbs).toHaveBeenCalledWith([]); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx index 5e8d972e1a135..2ae3ca0137d54 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, useEffect } from 'react'; +import React, { useEffect } from 'react'; +import { useValues } from 'kea'; import { useHistory } from 'react-router-dom'; import { EuiBreadcrumb } from '@elastic/eui'; -import { KibanaContext, IKibanaContext } from '../../index'; +import { KibanaLogic } from '../kibana'; + import { useEnterpriseSearchBreadcrumbs, useAppSearchBreadcrumbs, @@ -41,7 +43,7 @@ type TBreadcrumbsProps = IBreadcrumbsProps | IRootBreadcrumbsProps; export const SetEnterpriseSearchChrome: React.FC = ({ text, isRoot }) => { const history = useHistory(); - const { setBreadcrumbs, setDocTitle } = useContext(KibanaContext) as IKibanaContext; + const { setBreadcrumbs, setDocTitle } = useValues(KibanaLogic); const title = isRoot ? [] : [text]; const docTitle = enterpriseSearchTitle(title as TTitle | []); @@ -59,7 +61,7 @@ export const SetEnterpriseSearchChrome: React.FC = ({ text, i export const SetAppSearchChrome: React.FC = ({ text, isRoot }) => { const history = useHistory(); - const { setBreadcrumbs, setDocTitle } = useContext(KibanaContext) as IKibanaContext; + const { setBreadcrumbs, setDocTitle } = useValues(KibanaLogic); const title = isRoot ? [] : [text]; const docTitle = appSearchTitle(title as TTitle | []); @@ -77,7 +79,7 @@ export const SetAppSearchChrome: React.FC = ({ text, isRoot } export const SetWorkplaceSearchChrome: React.FC = ({ text, isRoot }) => { const history = useHistory(); - const { setBreadcrumbs, setDocTitle } = useContext(KibanaContext) as IKibanaContext; + const { setBreadcrumbs, setDocTitle } = useValues(KibanaLogic); const title = isRoot ? [] : [text]; const docTitle = workplaceSearchTitle(title as TTitle | []); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx index 0c7bac99085dd..eba632d86dc66 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../__mocks__/shallow_usecontext.mock'; +import '../../__mocks__/kea.mock'; import '../../__mocks__/react_router_history.mock'; import React from 'react'; import { shallow, mount } from 'enzyme'; import { EuiLink, EuiButton } from '@elastic/eui'; -import { mockKibanaContext, mockHistory } from '../../__mocks__'; +import { mockKibanaValues, mockHistory } from '../../__mocks__'; import { EuiReactRouterLink, EuiReactRouterButton } from './eui_link'; @@ -69,7 +69,7 @@ describe('EUI & React Router Component Helpers', () => { wrapper.find(EuiLink).simulate('click', simulatedEvent); expect(simulatedEvent.preventDefault).toHaveBeenCalled(); - expect(mockKibanaContext.navigateToUrl).toHaveBeenCalled(); + expect(mockKibanaValues.navigateToUrl).toHaveBeenCalled(); }); it('does not prevent default browser behavior on new tab/window clicks', () => { @@ -81,7 +81,7 @@ describe('EUI & React Router Component Helpers', () => { }; wrapper.find(EuiLink).simulate('click', simulatedEvent); - expect(mockKibanaContext.navigateToUrl).not.toHaveBeenCalled(); + expect(mockKibanaValues.navigateToUrl).not.toHaveBeenCalled(); }); it('calls inherited onClick actions in addition to default navigation', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx index e3b46632ddf9e..99314515f2734 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; +import { useValues } from 'kea'; import { useHistory } from 'react-router-dom'; import { EuiLink, EuiButton, EuiButtonProps, EuiLinkAnchorProps } from '@elastic/eui'; -import { KibanaContext, IKibanaContext } from '../../index'; +import { KibanaLogic } from '../../shared/kibana'; import { letBrowserHandleEvent } from './link_events'; /** @@ -33,7 +34,7 @@ export const EuiReactRouterHelper: React.FC = ({ children, }) => { const history = useHistory(); - const { navigateToUrl } = useContext(KibanaContext) as IKibanaContext; + const { navigateToUrl } = useValues(KibanaLogic); // Generate the correct link href (with basename etc. accounted for) const href = shouldNotCreateHref ? to : history.createHref({ pathname: to }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx index 0423ae61779af..802a10e3b3db7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { EuiSteps, EuiIcon, EuiLink } from '@elastic/eui'; -import { mountWithContext } from '../../__mocks__'; +import { mountWithIntl } from '../../__mocks__'; import { SetupGuide } from './'; @@ -27,7 +27,7 @@ describe('SetupGuide', () => { }); it('renders with optional auth links', () => { - const wrapper = mountWithContext( + const wrapper = mountWithIntl( { it('renders WorkplaceSearchUnconfigured when config.host is not set', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); + (useValues as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); const wrapper = shallow(); expect(wrapper.find(WorkplaceSearchUnconfigured)).toHaveLength(1); }); it('renders WorkplaceSearchConfigured when config.host set', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'some.url' } })); + (useValues as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'some.url' } })); const wrapper = shallow(); expect(wrapper.find(WorkplaceSearchConfigured)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index a68dfaf8ea471..4769358a3eb30 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { Route, Redirect, Switch } from 'react-router-dom'; import { useActions, useValues } from 'kea'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../common/constants'; import { IInitialAppData } from '../../../common/types'; -import { KibanaContext, IKibanaContext } from '../index'; +import { KibanaLogic } from '../shared/kibana'; import { HttpLogic } from '../shared/http'; import { AppLogic } from './app_logic'; import { Layout } from '../shared/layout'; @@ -24,7 +24,7 @@ import { NotFound } from '../shared/not_found'; import { Overview } from './views/overview'; export const WorkplaceSearch: React.FC = (props) => { - const { config } = useContext(KibanaContext) as IKibanaContext; + const { config } = useValues(KibanaLogic); return !config.host ? : ; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx index ab5cd7f0de90f..a757e187da098 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../../__mocks__/shallow_usecontext.mock'; - import React from 'react'; import { shallow } from 'enzyme'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx index d9b05c5da777d..d9d03245f6141 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../../__mocks__/shallow_usecontext.mock'; import './__mocks__/overview_logic.mock'; import { setMockValues } from './__mocks__'; diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx index 54066cee414d8..dc1c111dea006 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx @@ -109,7 +109,7 @@ export function SearchBar({ globalSearch, navigateToUrl }: Props) { complete: () => {}, }); }, - 250, + 350, [searchValue] ); diff --git a/x-pack/plugins/ingest_manager/common/constants/index.ts b/x-pack/plugins/ingest_manager/common/constants/index.ts index 519e2861cdc1d..bdc5714f7e2fe 100644 --- a/x-pack/plugins/ingest_manager/common/constants/index.ts +++ b/x-pack/plugins/ingest_manager/common/constants/index.ts @@ -13,3 +13,9 @@ export * from './epm'; export * from './output'; export * from './enrollment_api_key'; export * from './settings'; + +// TODO: This is the default `index.max_result_window` ES setting, which dictates +// the maximum amount of results allowed to be returned from a search. It's possible +// for the actual setting to differ from the default. Can we retrieve the real +// setting in the future? +export const SO_SEARCH_LIMIT = 10000; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts index 7ed2fed91aa93..0709eddaa52ec 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts @@ -71,7 +71,7 @@ export interface InstallPackageResponse { response: AssetReference[]; } -export interface IBulkInstallPackageError { +export interface IBulkInstallPackageHTTPError { name: string; statusCode: number; error: string | Error; @@ -86,7 +86,7 @@ export interface BulkInstallPackageInfo { } export interface BulkInstallPackagesResponse { - response: Array; + response: Array; } export interface BulkInstallPackagesRequest { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts index 185e1fa5eb0ce..b97d39bac920b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts @@ -7,6 +7,7 @@ export { PLUGIN_ID, EPM_API_ROUTES, AGENT_API_ROUTES, + SO_SEARCH_LIMIT, AGENT_POLICY_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/bulk_actions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/bulk_actions.tsx index 25684c9faf594..ee453b9e786f1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/bulk_actions.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/bulk_actions.tsx @@ -15,7 +15,8 @@ import { EuiIcon, EuiPortal, } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, FormattedNumber } from '@kbn/i18n/react'; +import { SO_SEARCH_LIMIT } from '../../../../constants'; import { Agent } from '../../../../types'; import { AgentReassignAgentPolicyFlyout, AgentUnenrollAgentModal } from '../../components'; @@ -153,11 +154,22 @@ export const AgentBulkActions: React.FunctionComponent<{ - + {totalAgents > SO_SEARCH_LIMIT ? ( + , + total: , + }} + /> + ) : ( + + )} {(selectionMode === 'manual' && selectedAgents.length) || @@ -184,7 +196,7 @@ export const AgentBulkActions: React.FunctionComponent<{ count: selectionMode === 'manual' ? selectedAgents.length - : totalAgents - totalInactiveAgents, + : Math.min(totalAgents - totalInactiveAgents, SO_SEARCH_LIMIT), }} /> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/agent_policy_selection.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/agent_policy_selection.tsx index 7f23c645f9a2e..874d42a8db095 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/agent_policy_selection.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/agent_policy_selection.tsx @@ -8,6 +8,7 @@ import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSelect, EuiSpacer, EuiText, EuiButtonEmpty } from '@elastic/eui'; +import { SO_SEARCH_LIMIT } from '../../../../constants'; import { AgentPolicy, GetEnrollmentAPIKeysResponse } from '../../../../types'; import { sendGetEnrollmentAPIKeys, useCore } from '../../../../hooks'; import { AgentPolicyPackageBadges } from '../agent_policy_package_badges'; @@ -98,7 +99,7 @@ export const EnrollmentStepAgentPolicy: React.FC = (props) => { try { const res = await sendGetEnrollmentAPIKeys({ page: 1, - perPage: 10000, + perPage: SO_SEARCH_LIMIT, }); if (res.error) { throw res.error; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_policy_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_policy_section.tsx index 617be92b3b1fe..e54eff1cbd4a5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_policy_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_policy_section.tsx @@ -15,6 +15,7 @@ import { } from '@elastic/eui'; import { OverviewPanel } from './overview_panel'; import { OverviewStats } from './overview_stats'; +import { SO_SEARCH_LIMIT } from '../../../constants'; import { useLink, useGetPackagePolicies } from '../../../hooks'; import { AgentPolicy } from '../../../types'; import { Loading } from '../../fleet/components'; @@ -25,7 +26,7 @@ export const OverviewPolicySection: React.FC<{ agentPolicies: AgentPolicy[] }> = const { getHref } = useLink(); const packagePoliciesRequest = useGetPackagePolicies({ page: 1, - perPage: 10000, + perPage: SO_SEARCH_LIMIT, }); return ( diff --git a/x-pack/plugins/ingest_manager/server/collectors/register.ts b/x-pack/plugins/ingest_manager/server/collectors/register.ts index 2be8eb22bc98c..cb39e6a5be579 100644 --- a/x-pack/plugins/ingest_manager/server/collectors/register.ts +++ b/x-pack/plugins/ingest_manager/server/collectors/register.ts @@ -50,9 +50,12 @@ export function registerIngestManagerUsageCollector( offline: { type: 'long' }, }, packages: { - name: { type: 'keyword' }, - version: { type: 'keyword' }, - enabled: { type: 'boolean' }, + type: 'array', + items: { + name: { type: 'keyword' }, + version: { type: 'keyword' }, + enabled: { type: 'boolean' }, + }, }, }, }); diff --git a/x-pack/plugins/ingest_manager/server/constants/index.ts b/x-pack/plugins/ingest_manager/server/constants/index.ts index d677b79bb46f8..3965e27da0542 100644 --- a/x-pack/plugins/ingest_manager/server/constants/index.ts +++ b/x-pack/plugins/ingest_manager/server/constants/index.ts @@ -31,6 +31,7 @@ export { SETTINGS_API_ROUTES, APP_API_ROUTES, // Saved object types + SO_SEARCH_LIMIT, AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_ACTION_SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index 7ae896c1f30a6..c55979d187f9d 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -13,7 +13,9 @@ import { GetCategoriesResponse, GetPackagesResponse, GetLimitedPackagesResponse, + BulkInstallPackageInfo, BulkInstallPackagesResponse, + IBulkInstallPackageHTTPError, } from '../../../common'; import { GetCategoriesRequestSchema, @@ -26,21 +28,21 @@ import { BulkUpgradePackagesFromRegistryRequestSchema, } from '../../types'; import { + BulkInstallResponse, + bulkInstallPackages, getCategories, getPackages, getFile, getPackageInfo, + handleInstallPackageFailure, installPackage, + isBulkInstallError, removeInstallation, getLimitedPackages, getInstallationObject, } from '../../services/epm/packages'; -import { defaultIngestErrorHandler } from '../../errors'; +import { defaultIngestErrorHandler, ingestErrorToResponseOptions } from '../../errors'; import { splitPkgKey } from '../../services/epm/registry'; -import { - handleInstallPackageFailure, - bulkInstallPackages, -} from '../../services/epm/packages/install'; export const getCategoriesHandler: RequestHandler< undefined, @@ -171,6 +173,21 @@ export const installPackageFromRegistryHandler: RequestHandler< } }; +const bulkInstallServiceResponseToHttpEntry = ( + result: BulkInstallResponse +): BulkInstallPackageInfo | IBulkInstallPackageHTTPError => { + if (isBulkInstallError(result)) { + const { statusCode, body } = ingestErrorToResponseOptions(result.error); + return { + name: result.name, + statusCode, + error: body.message, + }; + } else { + return result; + } +}; + export const bulkInstallPackagesFromRegistryHandler: RequestHandler< undefined, undefined, @@ -178,13 +195,14 @@ export const bulkInstallPackagesFromRegistryHandler: RequestHandler< > = async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; - const res = await bulkInstallPackages({ + const bulkInstalledResponses = await bulkInstallPackages({ savedObjectsClient, callCluster, packagesToUpgrade: request.body.packages, }); + const payload = bulkInstalledResponses.map(bulkInstallServiceResponseToHttpEntry); const body: BulkInstallPackagesResponse = { - response: res, + response: payload, }; return response.ok({ body }); }; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/bulk_install_packages.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/bulk_install_packages.ts new file mode 100644 index 0000000000000..af937c5593082 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/bulk_install_packages.ts @@ -0,0 +1,61 @@ +/* + * 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 { SavedObjectsClientContract } from 'src/core/server'; +import { CallESAsCurrentUser } from '../../../types'; +import * as Registry from '../registry'; +import { getInstallationObject } from './index'; +import { BulkInstallResponse, IBulkInstallPackageError, upgradePackage } from './install'; + +interface BulkInstallPackagesParams { + savedObjectsClient: SavedObjectsClientContract; + packagesToUpgrade: string[]; + callCluster: CallESAsCurrentUser; +} + +export async function bulkInstallPackages({ + savedObjectsClient, + packagesToUpgrade, + callCluster, +}: BulkInstallPackagesParams): Promise { + const installedAndLatestPromises = packagesToUpgrade.map((pkgToUpgrade) => + Promise.all([ + getInstallationObject({ savedObjectsClient, pkgName: pkgToUpgrade }), + Registry.fetchFindLatestPackage(pkgToUpgrade), + ]) + ); + const installedAndLatestResults = await Promise.allSettled(installedAndLatestPromises); + const installResponsePromises = installedAndLatestResults.map(async (result, index) => { + const pkgToUpgrade = packagesToUpgrade[index]; + if (result.status === 'fulfilled') { + const [installedPkg, latestPkg] = result.value; + return upgradePackage({ + savedObjectsClient, + callCluster, + installedPkg, + latestPkg, + pkgToUpgrade, + }); + } else { + return { name: pkgToUpgrade, error: result.reason }; + } + }); + const installResults = await Promise.allSettled(installResponsePromises); + const installResponses = installResults.map((result, index) => { + const pkgToUpgrade = packagesToUpgrade[index]; + if (result.status === 'fulfilled') { + return result.value; + } else { + return { name: pkgToUpgrade, error: result.reason }; + } + }); + + return installResponses; +} + +export function isBulkInstallError(test: any): test is IBulkInstallPackageError { + return 'error' in test && test.error instanceof Error; +} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts new file mode 100644 index 0000000000000..f0b487ad59774 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts @@ -0,0 +1,144 @@ +/* + * 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 { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types'; +import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; + +jest.mock('./install'); +jest.mock('./bulk_install_packages'); +jest.mock('./get'); + +import { bulkInstallPackages, isBulkInstallError } from './bulk_install_packages'; +const { ensureInstalledDefaultPackages } = jest.requireActual('./install'); +const { isBulkInstallError: actualIsBulkInstallError } = jest.requireActual( + './bulk_install_packages' +); +import { getInstallation } from './get'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { appContextService } from '../../app_context'; +import { createAppContextStartContractMock } from '../../../mocks'; + +// if we add this assertion, TS will type check the return value +// and the editor will also know about .mockImplementation, .mock.calls, etc +const mockedBulkInstallPackages = bulkInstallPackages as jest.MockedFunction< + typeof bulkInstallPackages +>; +const mockedIsBulkInstallError = isBulkInstallError as jest.MockedFunction< + typeof isBulkInstallError +>; +const mockedGetInstallation = getInstallation as jest.MockedFunction; + +// I was unable to get the actual implementation set in the `jest.mock()` call at the top to work +// so this will set the `isBulkInstallError` function back to the actual implementation +mockedIsBulkInstallError.mockImplementation(actualIsBulkInstallError); + +const mockInstallation: SavedObject = { + id: 'test-pkg', + references: [], + type: 'epm-packages', + attributes: { + id: 'test-pkg', + installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], + installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], + es_index_patterns: { pattern: 'pattern-name' }, + name: 'test package', + version: '1.0.0', + install_status: 'installed', + install_version: '1.0.0', + install_started_at: new Date().toISOString(), + }, +}; + +describe('ensureInstalledDefaultPackages', () => { + let soClient: jest.Mocked; + beforeEach(async () => { + soClient = savedObjectsClientMock.create(); + appContextService.start(createAppContextStartContractMock()); + }); + afterEach(async () => { + appContextService.stop(); + }); + it('should return an array of Installation objects when successful', async () => { + mockedGetInstallation.mockImplementation(async () => { + return mockInstallation.attributes; + }); + mockedBulkInstallPackages.mockImplementationOnce(async function () { + return [ + { + name: mockInstallation.attributes.name, + assets: [], + newVersion: '', + oldVersion: '', + statusCode: 200, + }, + ]; + }); + const resp = await ensureInstalledDefaultPackages(soClient, jest.fn()); + expect(resp).toEqual([mockInstallation.attributes]); + }); + it('should throw the first Error it finds', async () => { + class SomeCustomError extends Error {} + mockedGetInstallation.mockImplementation(async () => { + return mockInstallation.attributes; + }); + mockedBulkInstallPackages.mockImplementationOnce(async function () { + return [ + { + name: 'success one', + assets: [], + newVersion: '', + oldVersion: '', + statusCode: 200, + }, + { + name: 'success two', + assets: [], + newVersion: '', + oldVersion: '', + statusCode: 200, + }, + { + name: 'failure one', + error: new SomeCustomError('abc 123'), + }, + { + name: 'success three', + assets: [], + newVersion: '', + oldVersion: '', + statusCode: 200, + }, + { + name: 'failure two', + error: new Error('zzz'), + }, + ]; + }); + const installPromise = ensureInstalledDefaultPackages(soClient, jest.fn()); + expect.assertions(2); + expect(installPromise).rejects.toThrow(SomeCustomError); + expect(installPromise).rejects.toThrow('abc 123'); + }); + it('should throw an error when get installation returns undefined', async () => { + mockedGetInstallation.mockImplementation(async () => { + return undefined; + }); + mockedBulkInstallPackages.mockImplementationOnce(async function () { + return [ + { + name: 'undefined package', + assets: [], + newVersion: '', + oldVersion: '', + statusCode: 200, + }, + ]; + }); + const installPromise = ensureInstalledDefaultPackages(soClient, jest.fn()); + expect.assertions(1); + expect(installPromise).rejects.toThrow(); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts new file mode 100644 index 0000000000000..cce4b7fee8fd7 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { SavedObject } from 'src/core/server'; +import { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types'; +import { getInstallType } from './install'; + +const mockInstallation: SavedObject = { + id: 'test-pkg', + references: [], + type: 'epm-packages', + attributes: { + id: 'test-pkg', + installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], + installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], + es_index_patterns: { pattern: 'pattern-name' }, + name: 'test packagek', + version: '1.0.0', + install_status: 'installed', + install_version: '1.0.0', + install_started_at: new Date().toISOString(), + }, +}; +const mockInstallationUpdateFail: SavedObject = { + id: 'test-pkg', + references: [], + type: 'epm-packages', + attributes: { + id: 'test-pkg', + installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], + installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], + es_index_patterns: { pattern: 'pattern-name' }, + name: 'test packagek', + version: '1.0.0', + install_status: 'installing', + install_version: '1.0.1', + install_started_at: new Date().toISOString(), + }, +}; + +describe('getInstallType', () => { + it('should return correct type when installing and no other version is currently installed', () => { + const installTypeInstall = getInstallType({ pkgVersion: '1.0.0', installedPkg: undefined }); + expect(installTypeInstall).toBe('install'); + + // @ts-expect-error can only be 'install' if no installedPkg given + expect(installTypeInstall === 'update').toBe(false); + // @ts-expect-error can only be 'install' if no installedPkg given + expect(installTypeInstall === 'reinstall').toBe(false); + // @ts-expect-error can only be 'install' if no installedPkg given + expect(installTypeInstall === 'reupdate').toBe(false); + // @ts-expect-error can only be 'install' if no installedPkg given + expect(installTypeInstall === 'rollback').toBe(false); + }); + + it('should return correct type when installing the same version', () => { + const installTypeReinstall = getInstallType({ + pkgVersion: '1.0.0', + installedPkg: mockInstallation, + }); + expect(installTypeReinstall).toBe('reinstall'); + + // @ts-expect-error cannot be 'install' if given installedPkg + expect(installTypeReinstall === 'install').toBe(false); + }); + + it('should return correct type when moving from one version to another', () => { + const installTypeUpdate = getInstallType({ + pkgVersion: '1.0.1', + installedPkg: mockInstallation, + }); + expect(installTypeUpdate).toBe('update'); + + // @ts-expect-error cannot be 'install' if given installedPkg + expect(installTypeUpdate === 'install').toBe(false); + }); + + it('should return correct type when update fails and trys again', () => { + const installTypeReupdate = getInstallType({ + pkgVersion: '1.0.1', + installedPkg: mockInstallationUpdateFail, + }); + expect(installTypeReupdate).toBe('reupdate'); + + // @ts-expect-error cannot be 'install' if given installedPkg + expect(installTypeReupdate === 'install').toBe(false); + }); + + it('should return correct type when attempting to rollback from a failed update', () => { + const installTypeRollback = getInstallType({ + pkgVersion: '1.0.0', + installedPkg: mockInstallationUpdateFail, + }); + expect(installTypeRollback).toBe('rollback'); + + // @ts-expect-error cannot be 'install' if given installedPkg + expect(installTypeRollback === 'install').toBe(false); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts index 57c4f77432455..94aa969c2d2b8 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts @@ -12,6 +12,8 @@ import { InstallationStatus, KibanaAssetType, } from '../../../types'; + +export { bulkInstallPackages, isBulkInstallError } from './bulk_install_packages'; export { getCategories, getFile, @@ -23,7 +25,13 @@ export { SearchParams, } from './get'; -export { installPackage, ensureInstalledPackage } from './install'; +export { + BulkInstallResponse, + handleInstallPackageFailure, + installPackage, + IBulkInstallPackageError, + ensureInstalledPackage, +} from './install'; export { removeInstallation } from './remove'; type RequiredPackage = 'system' | 'endpoint'; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.test.ts deleted file mode 100644 index 2f60c74d3514f..0000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.test.ts +++ /dev/null @@ -1,103 +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 { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types'; -import { SavedObject } from 'src/core/server'; -import { getInstallType } from './install'; - -const mockInstallation: SavedObject = { - id: 'test-pkg', - references: [], - type: 'epm-packages', - attributes: { - id: 'test-pkg', - installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], - installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], - es_index_patterns: { pattern: 'pattern-name' }, - name: 'test packagek', - version: '1.0.0', - install_status: 'installed', - install_version: '1.0.0', - install_started_at: new Date().toISOString(), - }, -}; -const mockInstallationUpdateFail: SavedObject = { - id: 'test-pkg', - references: [], - type: 'epm-packages', - attributes: { - id: 'test-pkg', - installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], - installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], - es_index_patterns: { pattern: 'pattern-name' }, - name: 'test packagek', - version: '1.0.0', - install_status: 'installing', - install_version: '1.0.1', - install_started_at: new Date().toISOString(), - }, -}; -describe('install', () => { - describe('getInstallType', () => { - it('should return correct type when installing and no other version is currently installed', () => { - const installTypeInstall = getInstallType({ pkgVersion: '1.0.0', installedPkg: undefined }); - expect(installTypeInstall).toBe('install'); - - // @ts-expect-error can only be 'install' if no installedPkg given - expect(installTypeInstall === 'update').toBe(false); - // @ts-expect-error can only be 'install' if no installedPkg given - expect(installTypeInstall === 'reinstall').toBe(false); - // @ts-expect-error can only be 'install' if no installedPkg given - expect(installTypeInstall === 'reupdate').toBe(false); - // @ts-expect-error can only be 'install' if no installedPkg given - expect(installTypeInstall === 'rollback').toBe(false); - }); - - it('should return correct type when installing the same version', () => { - const installTypeReinstall = getInstallType({ - pkgVersion: '1.0.0', - installedPkg: mockInstallation, - }); - expect(installTypeReinstall).toBe('reinstall'); - - // @ts-expect-error cannot be 'install' if given installedPkg - expect(installTypeReinstall === 'install').toBe(false); - }); - - it('should return correct type when moving from one version to another', () => { - const installTypeUpdate = getInstallType({ - pkgVersion: '1.0.1', - installedPkg: mockInstallation, - }); - expect(installTypeUpdate).toBe('update'); - - // @ts-expect-error cannot be 'install' if given installedPkg - expect(installTypeUpdate === 'install').toBe(false); - }); - - it('should return correct type when update fails and trys again', () => { - const installTypeReupdate = getInstallType({ - pkgVersion: '1.0.1', - installedPkg: mockInstallationUpdateFail, - }); - expect(installTypeReupdate).toBe('reupdate'); - - // @ts-expect-error cannot be 'install' if given installedPkg - expect(installTypeReupdate === 'install').toBe(false); - }); - - it('should return correct type when attempting to rollback from a failed update', () => { - const installTypeRollback = getInstallType({ - pkgVersion: '1.0.0', - installedPkg: mockInstallationUpdateFail, - }); - expect(installTypeRollback).toBe('rollback'); - - // @ts-expect-error cannot be 'install' if given installedPkg - expect(installTypeRollback === 'install').toBe(false); - }); - }); -}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 800151a41a429..6187f683a3a80 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -8,7 +8,7 @@ import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; import semver from 'semver'; import Boom from 'boom'; import { UnwrapPromise } from '@kbn/utility-types'; -import { BulkInstallPackageInfo, IBulkInstallPackageError } from '../../../../common'; +import { BulkInstallPackageInfo } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE, MAX_TIME_COMPLETE_INSTALL } from '../../../constants'; import { AssetReference, @@ -23,7 +23,13 @@ import { } from '../../../types'; import { installIndexPatterns } from '../kibana/index_pattern/install'; import * as Registry from '../registry'; -import { getInstallation, getInstallationObject, isRequiredPackage } from './index'; +import { + getInstallation, + getInstallationObject, + isRequiredPackage, + bulkInstallPackages, + isBulkInstallError, +} from './index'; import { installTemplates } from '../elasticsearch/template/install'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; import { installPipelines, deletePreviousPipelines } from '../elasticsearch/ingest_pipeline/'; @@ -36,11 +42,7 @@ import { } from '../kibana/assets/install'; import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; import { deleteKibanaSavedObjectsAssets, removeInstallation } from './remove'; -import { - IngestManagerError, - PackageOutdatedError, - ingestErrorToResponseOptions, -} from '../../../errors'; +import { IngestManagerError, PackageOutdatedError } from '../../../errors'; import { getPackageSavedObjects } from './get'; import { installTransformForDataset } from '../elasticsearch/transform/install'; import { appContextService } from '../../app_context'; @@ -68,17 +70,27 @@ export async function ensureInstalledDefaultPackages( callCluster: CallESAsCurrentUser ): Promise { const installations = []; - for (const pkgName in DefaultPackages) { - if (!DefaultPackages.hasOwnProperty(pkgName)) continue; - const installation = ensureInstalledPackage({ - savedObjectsClient, - pkgName, - callCluster, - }); - installations.push(installation); + const bulkResponse = await bulkInstallPackages({ + savedObjectsClient, + packagesToUpgrade: Object.values(DefaultPackages), + callCluster, + }); + + for (const resp of bulkResponse) { + if (isBulkInstallError(resp)) { + throw resp.error; + } else { + installations.push(getInstallation({ savedObjectsClient, pkgName: resp.name })); + } } - return Promise.all(installations); + const retrievedInstallations = await Promise.all(installations); + return retrievedInstallations.map((installation, index) => { + if (!installation) { + throw new Error(`could not get installation ${bulkResponse[index].name}`); + } + return installation; + }); } export async function ensureInstalledPackage(options: { @@ -154,21 +166,11 @@ export async function handleInstallPackageFailure({ } } -type BulkInstallResponse = BulkInstallPackageInfo | IBulkInstallPackageError; -function bulkInstallErrorToOptions({ - pkgToUpgrade, - error, -}: { - pkgToUpgrade: string; +export interface IBulkInstallPackageError { + name: string; error: Error; -}): IBulkInstallPackageError { - const { statusCode, body } = ingestErrorToResponseOptions(error); - return { - name: pkgToUpgrade, - statusCode, - error: body.message, - }; } +export type BulkInstallResponse = BulkInstallPackageInfo | IBulkInstallPackageError; interface UpgradePackageParams { savedObjectsClient: SavedObjectsClientContract; @@ -177,7 +179,7 @@ interface UpgradePackageParams { latestPkg: UnwrapPromise>; pkgToUpgrade: string; } -async function upgradePackage({ +export async function upgradePackage({ savedObjectsClient, callCluster, installedPkg, @@ -207,7 +209,7 @@ async function upgradePackage({ installedPkg, callCluster, }); - return bulkInstallErrorToOptions({ pkgToUpgrade, error: installFailed }); + return { name: pkgToUpgrade, error: installFailed }; } } else { // package was already at the latest version @@ -223,51 +225,6 @@ async function upgradePackage({ } } -interface BulkInstallPackagesParams { - savedObjectsClient: SavedObjectsClientContract; - packagesToUpgrade: string[]; - callCluster: CallESAsCurrentUser; -} -export async function bulkInstallPackages({ - savedObjectsClient, - packagesToUpgrade, - callCluster, -}: BulkInstallPackagesParams): Promise { - const installedAndLatestPromises = packagesToUpgrade.map((pkgToUpgrade) => - Promise.all([ - getInstallationObject({ savedObjectsClient, pkgName: pkgToUpgrade }), - Registry.fetchFindLatestPackage(pkgToUpgrade), - ]) - ); - const installedAndLatestResults = await Promise.allSettled(installedAndLatestPromises); - const installResponsePromises = installedAndLatestResults.map(async (result, index) => { - const pkgToUpgrade = packagesToUpgrade[index]; - if (result.status === 'fulfilled') { - const [installedPkg, latestPkg] = result.value; - return upgradePackage({ - savedObjectsClient, - callCluster, - installedPkg, - latestPkg, - pkgToUpgrade, - }); - } else { - return bulkInstallErrorToOptions({ pkgToUpgrade, error: result.reason }); - } - }); - const installResults = await Promise.allSettled(installResponsePromises); - const installResponses = installResults.map((result, index) => { - const pkgToUpgrade = packagesToUpgrade[index]; - if (result.status === 'fulfilled') { - return result.value; - } else { - return bulkInstallErrorToOptions({ pkgToUpgrade, error: result.reason }); - } - }); - - return installResponses; -} - interface InstallPackageParams { savedObjectsClient: SavedObjectsClientContract; pkgkey: string; diff --git a/x-pack/plugins/ingest_manager/server/services/saved_object.ts b/x-pack/plugins/ingest_manager/server/services/saved_object.ts index 06772206d5198..77c0e446d5c23 100644 --- a/x-pack/plugins/ingest_manager/server/services/saved_object.ts +++ b/x-pack/plugins/ingest_manager/server/services/saved_object.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { SavedObjectsClientContract, SavedObjectsFindResponse } from 'src/core/server'; +import { SO_SEARCH_LIMIT } from '../constants'; import { ListWithKuery } from '../types'; /** @@ -40,19 +41,13 @@ export const findAllSOs = async ( const { type, sortField, sortOrder, kuery } = options; let savedObjectResults: SavedObjectsFindResponse['saved_objects'] = []; - // TODO: This is the default `index.max_result_window` ES setting, which dictates - // the maximum amount of results allowed to be returned from a search. It's possible - // for the actual setting to differ from the default. Can we retrieve the real - // setting in the future? - const searchLimit = 10000; - const query = { type, sortField, sortOrder, filter: kuery, page: 1, - perPage: searchLimit, + perPage: SO_SEARCH_LIMIT, }; const { saved_objects: initialSOs, total } = await soClient.find(query); diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index f02057bae1598..c7ecf843d6a51 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -22,6 +22,7 @@ import { Output, DEFAULT_AGENT_POLICIES_PACKAGES, } from '../../common'; +import { SO_SEARCH_LIMIT } from '../constants'; import { getPackageInfo } from './epm/packages'; import { packagePolicyService } from './package_policy'; import { generateEnrollmentAPIKey } from './api_keys'; @@ -159,7 +160,7 @@ export async function setupFleet( }); const { items: agentPolicies } = await agentPolicyService.list(soClient, { - perPage: 10000, + perPage: SO_SEARCH_LIMIT, }); await Promise.all( 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 2f64a36e0462e..2572f732aa1b3 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 @@ -17,7 +17,7 @@ import { } from '@elastic/eui'; import { EuiFormLabel } from '@elastic/eui'; import { IndexPatternColumn, OperationType } from '../indexpattern'; -import { IndexPatternDimensionEditorProps, OperationFieldSupportMatrix } from './dimension_panel'; +import { IndexPatternDimensionEditorProps, OperationSupportMatrix } from './dimension_panel'; import { operationDefinitionMap, getOperationDisplay, @@ -36,7 +36,7 @@ const operationPanels = getOperationDisplay(); export interface DimensionEditorProps extends IndexPatternDimensionEditorProps { selectedColumn?: IndexPatternColumn; - operationFieldSupportMatrix: OperationFieldSupportMatrix; + operationSupportMatrix: OperationSupportMatrix; currentIndexPattern: IndexPattern; } @@ -90,7 +90,7 @@ const LabelInput = ({ value, onChange }: { value: string; onChange: (value: stri export function DimensionEditor(props: DimensionEditorProps) { const { selectedColumn, - operationFieldSupportMatrix, + operationSupportMatrix, state, columnId, setState, @@ -98,14 +98,16 @@ export function DimensionEditor(props: DimensionEditorProps) { currentIndexPattern, hideGrouping, } = props; - const { operationByField, fieldByOperation } = operationFieldSupportMatrix; + const { operationByField, fieldByOperation } = operationSupportMatrix; const [ incompatibleSelectedOperationType, setInvalidOperationType, ] = useState(null); - const ParamEditor = - selectedColumn && operationDefinitionMap[selectedColumn.operationType].paramEditor; + const selectedOperationDefinition = + selectedColumn && operationDefinitionMap[selectedColumn.operationType]; + + const ParamEditor = selectedOperationDefinition?.paramEditor; const fieldMap: Record = useMemo(() => { const fields: Record = {}; @@ -129,6 +131,10 @@ export function DimensionEditor(props: DimensionEditorProps) { [ ...asOperationOptions(validOperationTypes, true), ...asOperationOptions(possibleOperationTypes, false), + ...asOperationOptions( + operationSupportMatrix.operationWithoutField, + !selectedColumn || !hasField(selectedColumn) + ), ], 'operationType' ); @@ -166,12 +172,30 @@ export function DimensionEditor(props: DimensionEditorProps) { compatibleWithCurrentField ? '' : ' incompatible' }`, onClick() { - // todo: when moving from terms agg to filters, we want to create a filter `$field.name : *` - // it probably has to be re-thought when removing the field name. - const isTermsToFilters = - selectedColumn?.operationType === 'terms' && operationType === 'filters'; - - if (!selectedColumn || !compatibleWithCurrentField) { + if (operationDefinitionMap[operationType].input === 'none') { + // Clear invalid state because we are creating a valid column + setInvalidOperationType(null); + if (selectedColumn?.operationType === operationType) { + return; + } + setState( + changeColumn({ + state, + layerId, + columnId, + newColumn: buildColumn({ + columns: props.state.layers[props.layerId].columns, + suggestedPriority: props.suggestedPriority, + layerId: props.layerId, + op: operationType, + indexPattern: currentIndexPattern, + previousColumn: selectedColumn, + }), + }) + ); + trackUiEvent(`indexpattern_dimension_operation_${operationType}`); + return; + } else if (!selectedColumn || !compatibleWithCurrentField) { const possibleFields = fieldByOperation[operationType] || []; if (possibleFields.length === 1) { @@ -197,19 +221,20 @@ export function DimensionEditor(props: DimensionEditorProps) { trackUiEvent(`indexpattern_dimension_operation_${operationType}`); return; } - if (incompatibleSelectedOperationType && !isTermsToFilters) { - setInvalidOperationType(null); - } - if (selectedColumn.operationType === operationType) { + + setInvalidOperationType(null); + + if (selectedColumn?.operationType === operationType) { return; } + const newColumn: IndexPatternColumn = buildColumn({ columns: props.state.layers[props.layerId].columns, suggestedPriority: props.suggestedPriority, layerId: props.layerId, op: operationType, indexPattern: currentIndexPattern, - field: fieldMap[selectedColumn.sourceField], + field: hasField(selectedColumn) ? fieldMap[selectedColumn.sourceField] : undefined, previousColumn: selectedColumn, }); @@ -244,93 +269,101 @@ export function DimensionEditor(props: DimensionEditorProps) {
- - { - setState( - deleteColumn({ - state, - layerId, - columnId, - }) - ); - }} - onChoose={(choice) => { - let column: IndexPatternColumn; - if ( - !incompatibleSelectedOperationType && - selectedColumn && - 'field' in choice && - choice.operationType === selectedColumn.operationType - ) { - // If we just changed the field are not in an error state and the operation didn't change, - // we use the operations onFieldChange method to calculate the new column. - column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]); - } else { - // Otherwise we'll use the buildColumn method to calculate a new column - const compatibleOperations = - ('field' in choice && - operationFieldSupportMatrix.operationByField[choice.field]) || - []; - let operation; - if (compatibleOperations.length > 0) { - operation = - incompatibleSelectedOperationType && - compatibleOperations.includes(incompatibleSelectedOperationType) - ? incompatibleSelectedOperationType - : compatibleOperations[0]; - } else if ('field' in choice) { - operation = choice.operationType; - } - column = buildColumn({ - columns: props.state.layers[props.layerId].columns, - field: fieldMap[choice.field], - indexPattern: currentIndexPattern, - layerId: props.layerId, - suggestedPriority: props.suggestedPriority, - op: operation as OperationType, - previousColumn: selectedColumn, - }); + > + { + setState( + deleteColumn({ + state, + layerId, + columnId, + }) + ); + }} + onChoose={(choice) => { + let column: IndexPatternColumn; + if ( + !incompatibleSelectedOperationType && + selectedColumn && + 'field' in choice && + choice.operationType === selectedColumn.operationType + ) { + // If we just changed the field are not in an error state and the operation didn't change, + // we use the operations onFieldChange method to calculate the new column. + column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]); + } else { + // Otherwise we'll use the buildColumn method to calculate a new column + const compatibleOperations = + ('field' in choice && operationSupportMatrix.operationByField[choice.field]) || + []; + let operation; + if (compatibleOperations.length > 0) { + operation = + incompatibleSelectedOperationType && + compatibleOperations.includes(incompatibleSelectedOperationType) + ? incompatibleSelectedOperationType + : compatibleOperations[0]; + } else if ('field' in choice) { + operation = choice.operationType; + } + column = buildColumn({ + columns: props.state.layers[props.layerId].columns, + field: fieldMap[choice.field], + indexPattern: currentIndexPattern, + layerId: props.layerId, + suggestedPriority: props.suggestedPriority, + op: operation as OperationType, + previousColumn: selectedColumn, + }); + } - setState( - changeColumn({ - state, - layerId, - columnId, - newColumn: column, - keepParams: false, - }) - ); - setInvalidOperationType(null); - }} - /> - + setState( + changeColumn({ + state, + layerId, + columnId, + newColumn: column, + keepParams: false, + }) + ); + setInvalidOperationType(null); + }} + /> + + ) : null} - {!incompatibleSelectedOperationType && ParamEditor && ( + {!incompatibleSelectedOperationType && selectedColumn && ParamEditor && ( <> { let state: IndexPatternPrivateState; let setState: jest.Mock; let defaultProps: IndexPatternDimensionEditorProps; let dragDropContext: DragContextState; + function getStateWithColumns(columns: Record) { + return { ...state, layers: { first: { ...state.layers.first, columns } } }; + } + beforeEach(() => { state = { indexPatternRefs: [], @@ -179,7 +207,7 @@ describe('IndexPatternDimensionEditorPanel', () => { expect(filterOperations).toBeCalled(); }); - it('should show field select combo box on click', () => { + it('should show field select', () => { wrapper = mount(); expect( @@ -187,6 +215,29 @@ describe('IndexPatternDimensionEditorPanel', () => { ).toHaveLength(1); }); + it('should not show field select on fieldless operation', () => { + wrapper = mount( + + ); + + expect( + wrapper.find(EuiComboBox).filter('[data-test-subj="indexPattern-dimension-field"]') + ).toHaveLength(0); + }); + it('should not show any choices if the filter returns false', () => { wrapper = mount( { wrapper = mount( ); @@ -292,26 +324,7 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper = mount( ); @@ -324,30 +337,15 @@ describe('IndexPatternDimensionEditorPanel', () => { expect(items.find(({ label }) => label === 'Date histogram')!['data-test-subj']).toContain( 'incompatible' ); + + // Fieldless operation is compatible with field + expect(items.find(({ label }) => label === 'Filters')!['data-test-subj']).toContain( + 'compatible' + ); }); it('should keep the operation when switching to another field compatible with this operation', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: { - label: 'Max of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'max', - sourceField: 'bytes', - params: { format: { id: 'bytes' } }, - }, - }, - }, - }, - }; + const initialState: IndexPatternPrivateState = getStateWithColumns({ col1: bytesColumn }); wrapper = mount( @@ -415,27 +413,7 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper = mount( ); @@ -505,27 +483,7 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper = mount( ); @@ -553,28 +511,13 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper = mount( ); @@ -640,6 +583,62 @@ describe('IndexPatternDimensionEditorPanel', () => { expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); }); + it('should leave error state if the original operation is re-selected', () => { + wrapper = mount(); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') + .simulate('click'); + + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); + }); + + it('should leave error state when switching from incomplete state to fieldless operation', () => { + wrapper = mount(); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-filters incompatible"]') + .simulate('click'); + + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); + }); + + it('should leave error state when re-selecting the original fieldless function', () => { + wrapper = mount( + + ); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-filters"]') + .simulate('click'); + + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); + }); + it('should indicate fields compatible with selected operation', () => { wrapper = mount(); @@ -701,28 +700,18 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('should select the Records field when count is selected', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: { - dataType: 'number', - isBucketed: false, - label: '', - operationType: 'avg', - sourceField: 'bytes', - }, - }, - }, - }, - }; wrapper = mount( ); @@ -737,28 +726,18 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('should indicate document and field compatibility with selected document operation', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: { - dataType: 'number', - isBucketed: false, - label: '', - operationType: 'count', - sourceField: 'Records', - }, - }, - }, - }, - }; wrapper = mount( ); @@ -942,28 +921,18 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('should indicate document compatibility when document operation is selected', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: { - dataType: 'number', - isBucketed: false, - label: '', - operationType: 'count', - sourceField: 'Records', - }, - }, - }, - }, - }; wrapper = mount( ); @@ -1031,26 +1000,9 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('should use helper function when changing the function', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: { - label: 'Max of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'max', - sourceField: 'bytes', - }, - }, - }, - }, - }; + const initialState: IndexPatternPrivateState = getStateWithColumns({ + col1: bytesColumn, + }); wrapper = mount( ); @@ -1095,25 +1047,16 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('allows custom format', () => { - const stateWithNumberCol: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Average of memory', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'memory', - }, - }, - }, + const stateWithNumberCol: IndexPatternPrivateState = getStateWithColumns({ + col1: { + label: 'Average of memory', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'memory', }, - }; + }); wrapper = mount( @@ -1145,29 +1088,19 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('keeps decimal places while switching', () => { - const stateWithNumberCol: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Average of memory', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'memory', - params: { - format: { id: 'bytes', params: { decimals: 0 } }, - }, - }, - }, + const stateWithNumberCol: IndexPatternPrivateState = getStateWithColumns({ + col1: { + label: 'Average of memory', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'memory', + params: { + format: { id: 'bytes', params: { decimals: 0 } }, }, }, - }; - + }); wrapper = mount( ); @@ -1195,28 +1128,19 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('allows custom format with number of decimal places', () => { - const stateWithNumberCol: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Average of memory', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'memory', - params: { - format: { id: 'bytes', params: { decimals: 2 } }, - }, - }, - }, + const stateWithNumberCol: IndexPatternPrivateState = getStateWithColumns({ + col1: { + label: 'Average of memory', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'memory', + params: { + format: { id: 'bytes', params: { decimals: 2 } }, }, }, - }; + }); wrapper = mount( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index 923f7145d1c64..c4d8300722f83 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -46,8 +46,9 @@ export type IndexPatternDimensionEditorProps = DatasourceDimensionEditorProps< dateRange: DateRange; }; -export interface OperationFieldSupportMatrix { +export interface OperationSupportMatrix { operationByField: Partial>; + operationWithoutField: OperationType[]; fieldByOperation: Partial>; } @@ -58,7 +59,7 @@ type Props = Pick< // TODO: This code has historically been memoized, as a potentially performance // sensitive task. If we can add memoization without breaking the behavior, we should. -const getOperationFieldSupportMatrix = (props: Props): OperationFieldSupportMatrix => { +const getOperationSupportMatrix = (props: Props): OperationSupportMatrix => { const layerId = props.layerId; const currentIndexPattern = props.state.indexPatterns[props.state.layers[layerId].indexPatternId]; @@ -67,37 +68,43 @@ const getOperationFieldSupportMatrix = (props: Props): OperationFieldSupportMatr ).filter((operation) => props.filterOperations(operation.operationMetaData)); const supportedOperationsByField: Partial> = {}; + const supportedOperationsWithoutField: OperationType[] = []; const supportedFieldsByOperation: Partial> = {}; filteredOperationsByMetadata.forEach(({ operations }) => { operations.forEach((operation) => { - if (supportedOperationsByField[operation.field]) { - supportedOperationsByField[operation.field]!.push(operation.operationType); - } else { - supportedOperationsByField[operation.field] = [operation.operationType]; - } - - if (supportedFieldsByOperation[operation.operationType]) { - supportedFieldsByOperation[operation.operationType]!.push(operation.field); - } else { - supportedFieldsByOperation[operation.operationType] = [operation.field]; + if (operation.type === 'field') { + if (supportedOperationsByField[operation.field]) { + supportedOperationsByField[operation.field]!.push(operation.operationType); + } else { + supportedOperationsByField[operation.field] = [operation.operationType]; + } + + if (supportedFieldsByOperation[operation.operationType]) { + supportedFieldsByOperation[operation.operationType]!.push(operation.field); + } else { + supportedFieldsByOperation[operation.operationType] = [operation.field]; + } + } else if (operation.type === 'none') { + supportedOperationsWithoutField.push(operation.operationType); } }); }); return { operationByField: _.mapValues(supportedOperationsByField, _.uniq), + operationWithoutField: _.uniq(supportedOperationsWithoutField), fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq), }; }; export function canHandleDrop(props: DatasourceDimensionDropProps) { - const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); + const operationSupportMatrix = getOperationSupportMatrix(props); const { dragging } = props.dragDropContext; const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId; function hasOperationForField(field: IndexPatternField) { - return Boolean(operationFieldSupportMatrix.operationByField[field.name]); + return Boolean(operationSupportMatrix.operationByField[field.name]); } if (isDraggedField(dragging)) { @@ -119,11 +126,11 @@ export function canHandleDrop(props: DatasourceDimensionDropProps) { - const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); + const operationSupportMatrix = getOperationSupportMatrix(props); const droppedItem = props.droppedItem; function hasOperationForField(field: IndexPatternField) { - return Boolean(operationFieldSupportMatrix.operationByField[field.name]); + return Boolean(operationSupportMatrix.operationByField[field.name]); } if (isDraggedOperation(droppedItem) && droppedItem.layerId === props.layerId) { @@ -167,8 +174,7 @@ export function onDrop(props: DatasourceDimensionDropHandlerProps ); }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index e71a85868b855..de472cb09cdfe 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -20,7 +20,7 @@ import { EuiHighlight } from '@elastic/eui'; import { OperationType } from '../indexpattern'; import { LensFieldIcon } from '../lens_field_icon'; import { DataType } from '../../types'; -import { OperationFieldSupportMatrix } from './dimension_panel'; +import { OperationSupportMatrix } from './dimension_panel'; import { IndexPattern, IndexPatternField, IndexPatternPrivateState } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { fieldExists } from '../pure_helpers'; @@ -37,7 +37,7 @@ export interface FieldSelectProps extends EuiComboBoxProps<{}> { incompatibleSelectedOperationType: OperationType | null; selectedColumnOperationType?: OperationType; selectedColumnSourceField?: string; - operationFieldSupportMatrix: OperationFieldSupportMatrix; + operationSupportMatrix: OperationSupportMatrix; onChoose: (choice: FieldChoice) => void; onDeleteColumn: () => void; existingFields: IndexPatternPrivateState['existingFields']; @@ -49,13 +49,13 @@ export function FieldSelect({ incompatibleSelectedOperationType, selectedColumnOperationType, selectedColumnSourceField, - operationFieldSupportMatrix, + operationSupportMatrix, onChoose, onDeleteColumn, existingFields, ...rest }: FieldSelectProps) { - const { operationByField } = operationFieldSupportMatrix; + const { operationByField } = operationSupportMatrix; const memoizedFieldOptions = useMemo(() => { const fields = Object.keys(operationByField).sort(); @@ -173,15 +173,13 @@ export function FieldSelect({ options={(memoizedFieldOptions as unknown) as EuiComboBoxOptionOption[]} isInvalid={Boolean(incompatibleSelectedOperationType)} selectedOptions={ - ((selectedColumnOperationType - ? selectedColumnSourceField - ? [ - { - label: fieldMap[selectedColumnSourceField].displayName, - value: { type: 'field', field: selectedColumnSourceField }, - }, - ] - : [memoizedFieldOptions[0]] + ((selectedColumnOperationType && selectedColumnSourceField + ? [ + { + label: fieldMap[selectedColumnSourceField].displayName, + value: { type: 'field', field: selectedColumnSourceField }, + }, + ] : []) as unknown) as EuiComboBoxOptionOption[] } singleSelection={{ asPlainText: true }} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index f3aa9c4f51c82..f5e64149c2c76 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -483,11 +483,15 @@ function createChangedNestingSuggestion(state: IndexPatternPrivateState, layerId const updatedLayer = { ...layer, columnOrder: [secondBucket, firstBucket, ...rest] }; const currentFields = state.indexPatterns[state.currentIndexPatternId].fields; const firstBucketLabel = - currentFields.find((field) => field.name === layer.columns[firstBucket].sourceField) - ?.displayName || ''; + currentFields.find((field) => { + const column = layer.columns[firstBucket]; + return hasField(column) && column.sourceField === field.name; + })?.displayName || ''; const secondBucketLabel = - currentFields.find((field) => field.name === layer.columns[secondBucket].sourceField) - ?.displayName || ''; + currentFields.find((field) => { + const column = layer.columns[secondBucket]; + return hasField(column) && column.sourceField === field.name; + })?.displayName || ''; return buildSuggestion({ state, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index b0777c7febd7d..65119d3978ee6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { OperationDefinition } from './index'; -import { FormattedIndexPatternColumn } from './column_types'; +import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types'; const supportedTypes = new Set(['string', 'boolean', 'number', 'ip', 'date']); @@ -21,15 +21,18 @@ function ofName(name: string) { }); } -export interface CardinalityIndexPatternColumn extends FormattedIndexPatternColumn { +export interface CardinalityIndexPatternColumn + extends FormattedIndexPatternColumn, + FieldBasedIndexPatternColumn { operationType: 'cardinality'; } -export const cardinalityOperation: OperationDefinition = { +export const cardinalityOperation: OperationDefinition = { type: OPERATION_TYPE, displayName: i18n.translate('xpack.lens.indexPattern.cardinality', { defaultMessage: 'Unique count', }), + input: 'field', getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { if ( supportedTypes.has(type) && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts index 3244eeb94d1e2..2e95e3fd4250f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts @@ -14,7 +14,6 @@ import { Operation, DimensionPriority } from '../../../types'; export interface BaseIndexPatternColumn extends Operation { // Private operationType: string; - sourceField: string; suggestedPriority?: DimensionPriority; customLabel?: boolean; } @@ -31,23 +30,6 @@ export interface FormattedIndexPatternColumn extends BaseIndexPatternColumn { }; } -/** - * Base type for a column that doesn't have additional parameter. - * - * * `TOperationType` should be a string type containing just the type - * of the operation (e.g. `"sum"`). - * * `TBase` is the base column interface the operation type is set for - - * by default this is `FieldBasedIndexPatternColumn`, so - * `ParameterlessIndexPatternColumn<'foo'>` will give you a column type - * for an operation named foo that operates on a field. - * By passing in another `TBase` (e.g. just `BaseIndexPatternColumn`), - * you can also create other column types. - */ -export type ParameterlessIndexPatternColumn< - TOperationType extends string, - TBase extends BaseIndexPatternColumn = FieldBasedIndexPatternColumn -> = TBase & { operationType: TOperationType }; - export interface FieldBasedIndexPatternColumn extends BaseIndexPatternColumn { - suggestedPriority?: DimensionPriority; + sourceField: string; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx index bb1aef856de78..cdf1a6b760493 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx @@ -6,23 +6,25 @@ import { i18n } from '@kbn/i18n'; import { OperationDefinition } from './index'; -import { FormattedIndexPatternColumn } from './column_types'; +import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types'; import { IndexPatternField } from '../../types'; const countLabel = i18n.translate('xpack.lens.indexPattern.countOf', { defaultMessage: 'Count of records', }); -export type CountIndexPatternColumn = FormattedIndexPatternColumn & { - operationType: 'count'; -}; +export type CountIndexPatternColumn = FormattedIndexPatternColumn & + FieldBasedIndexPatternColumn & { + operationType: 'count'; + }; -export const countOperation: OperationDefinition = { +export const countOperation: OperationDefinition = { type: 'count', priority: 2, displayName: i18n.translate('xpack.lens.indexPattern.count', { defaultMessage: 'Count', }), + input: 'field', onFieldChange: (oldColumn, indexPattern, field) => { return { ...oldColumn, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx index 7784024b03132..185f44405bb4b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx @@ -36,11 +36,15 @@ export interface DateHistogramIndexPatternColumn extends FieldBasedIndexPatternC }; } -export const dateHistogramOperation: OperationDefinition = { +export const dateHistogramOperation: OperationDefinition< + DateHistogramIndexPatternColumn, + 'field' +> = { type: 'date_histogram', displayName: i18n.translate('xpack.lens.indexPattern.dateHistogram', { defaultMessage: 'Date histogram', }), + input: 'field', priority: 5, // Highest priority level used getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { if ( @@ -136,7 +140,7 @@ export const dateHistogramOperation: OperationDefinition { + paramEditor: ({ state, setState, currentColumn, layerId, dateRange, data }) => { const field = currentColumn && state.indexPatterns[state.layers[layerId].indexPatternId].fields.find( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx index 2d79c5faf74fe..3ac01886537dc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx @@ -59,7 +59,6 @@ describe('filters', () => { operationType: 'filters', scale: 'ordinal', isBucketed: true, - sourceField: 'Records', params: { filters: [ { @@ -112,34 +111,14 @@ describe('filters', () => { }); }); - describe('getPossibleOperationForField', () => { + describe('getPossibleOperation', () => { it('should return operation with the right type for document', () => { - expect( - filtersOperation.getPossibleOperationForField({ - aggregatable: true, - searchable: true, - name: 'test', - displayName: 'test', - type: 'document', - }) - ).toEqual({ + expect(filtersOperation.getPossibleOperation()).toEqual({ dataType: 'string', isBucketed: true, scale: 'ordinal', }); }); - - it('should not return operation if field type is not document', () => { - expect( - filtersOperation.getPossibleOperationForField({ - aggregatable: false, - searchable: true, - name: 'test', - displayName: 'test', - type: 'string', - }) - ).toEqual(undefined); - }); }); describe('popover param editor', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx index 9985ad7229ecc..ad0b9f2dbb0ab 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiLink, htmlIdGenerator } from '@elastic/eui'; import { updateColumnParam } from '../../../state_helpers'; import { OperationDefinition } from '../index'; -import { FieldBasedIndexPatternColumn } from '../column_types'; +import { BaseIndexPatternColumn } from '../column_types'; import { FilterPopover } from './filter_popover'; import { IndexPattern } from '../../../types'; import { Query, esKuery, esQuery } from '../../../../../../../../src/plugins/data/public'; @@ -61,31 +61,22 @@ export const isQueryValid = (input: Query, indexPattern: IndexPattern) => { } }; -export interface FiltersIndexPatternColumn extends FieldBasedIndexPatternColumn { +export interface FiltersIndexPatternColumn extends BaseIndexPatternColumn { operationType: 'filters'; params: { filters: Filter[]; }; } -export const filtersOperation: OperationDefinition = { +export const filtersOperation: OperationDefinition = { type: 'filters', displayName: filtersLabel, priority: 3, // Higher than any metric - getPossibleOperationForField: ({ type }) => { - if (type === 'document') { - return { - dataType: 'string', - isBucketed: true, - scale: 'ordinal', - }; - } - }, - isTransferable: () => false, - onFieldChange: (oldColumn, indexPattern, field) => oldColumn, + input: 'none', + isTransferable: () => true, - buildColumn({ suggestedPriority, field, previousColumn }) { + buildColumn({ suggestedPriority, previousColumn }) { let params = { filters: [defaultFilter] }; if (previousColumn?.operationType === 'terms') { params = { @@ -108,11 +99,18 @@ export const filtersOperation: OperationDefinition = scale: 'ordinal', suggestedPriority, isBucketed: true, - sourceField: field.name, params, }; }, + getPossibleOperation() { + return { + dataType: 'string', + isBucketed: true, + scale: 'ordinal', + }; + }, + toEsAggsConfig: (column, columnId, indexPattern) => { const validFilters = column.params.filters?.filter((f: Filter) => isQueryValid(f.input, indexPattern) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 19523b550af5a..38aec866ca5cb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -28,22 +28,6 @@ import { DateRange } from '../../../../common'; import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; import { RangeIndexPatternColumn, rangeOperation } from './ranges'; -// List of all operation definitions registered to this data source. -// If you want to implement a new operation, add the definition to this array and -// the column type to the `IndexPatternColumn` union type below. -const internalOperationDefinitions = [ - filtersOperation, - termsOperation, - dateHistogramOperation, - minOperation, - maxOperation, - averageOperation, - cardinalityOperation, - sumOperation, - countOperation, - rangeOperation, -]; - /** * A union type of all available column types. If a column is of an unknown type somewhere * withing the indexpattern data source it should be typed as `IndexPatternColumn` to make @@ -61,6 +45,24 @@ export type IndexPatternColumn = | SumIndexPatternColumn | CountIndexPatternColumn; +export type FieldBasedIndexPatternColumn = Extract; + +// List of all operation definitions registered to this data source. +// If you want to implement a new operation, add the definition to this array and +// the column type to the `IndexPatternColumn` union type below. +const internalOperationDefinitions = [ + filtersOperation, + termsOperation, + dateHistogramOperation, + minOperation, + maxOperation, + averageOperation, + cardinalityOperation, + sumOperation, + countOperation, + rangeOperation, +]; + export { termsOperation } from './terms'; export { rangeOperation } from './ranges'; export { filtersOperation } from './filters'; @@ -71,7 +73,7 @@ export { countOperation } from './count'; /** * Properties passed to the operation-specific part of the popover editor */ -export interface ParamEditorProps { +export interface ParamEditorProps { currentColumn: C; state: IndexPatternPrivateState; setState: StateSetter; @@ -138,13 +140,25 @@ interface BaseBuildColumnArgs { indexPattern: IndexPattern; } -/** - * Shape of an operation definition. If the type parameter of the definition - * indicates a field based column, `getPossibleOperationForField` has to be - * specified, otherwise `getPossibleOperationForDocument` has to be defined. - */ -export interface OperationDefinition - extends BaseOperationDefinitionProps { +interface FieldlessOperationDefinition { + input: 'none'; + /** + * Builds the column object for the given parameters. Should include default p + */ + buildColumn: ( + arg: BaseBuildColumnArgs & { + previousColumn?: IndexPatternColumn; + } + ) => C; + /** + * Returns the meta data of the operation if applied. Undefined + * if the field is not applicable. + */ + getPossibleOperation: () => OperationMetadata | undefined; +} + +interface FieldBasedOperationDefinition { + input: 'field'; /** * Returns the meta data of the operation if applied to the given field. Undefined * if the field is not applicable to the operation. @@ -156,7 +170,8 @@ export interface OperationDefinition buildColumn: ( arg: BaseBuildColumnArgs & { field: IndexPatternField; - previousColumn?: IndexPatternColumn; + // previousColumn?: IndexPatternColumn; + previousColumn?: C; } ) => C; /** @@ -175,9 +190,29 @@ export interface OperationDefinition * @param indexPattern The index pattern that field is on. * @param field The field that the user changed to. */ - onFieldChange: (oldColumn: C, indexPattern: IndexPattern, field: IndexPatternField) => C; + onFieldChange: ( + // oldColumn: FieldBasedIndexPatternColumn, + oldColumn: C, + indexPattern: IndexPattern, + field: IndexPatternField + ) => C; } +interface OperationDefinitionMap { + field: FieldBasedOperationDefinition; + none: FieldlessOperationDefinition; +} + +/** + * Shape of an operation definition. If the type parameter of the definition + * indicates a field based column, `getPossibleOperationForField` has to be + * specified, otherwise `getPossibleOperation` has to be defined. + */ +export type OperationDefinition< + C extends BaseIndexPatternColumn, + Input extends keyof OperationDefinitionMap +> = BaseOperationDefinitionProps & OperationDefinitionMap[Input]; + /** * A union type of all available operation types. The operation type is a unique id of an operation. * Each column is assigned to exactly one operation type. @@ -188,7 +223,9 @@ export type OperationType = typeof internalOperationDefinitions[number]['type']; * This is an operation definition of an unspecified column out of all possible * column types. */ -export type GenericOperationDefinition = OperationDefinition; +export type GenericOperationDefinition = + | OperationDefinition + | OperationDefinition; /** * List of all available operation definitions @@ -206,7 +243,10 @@ export const operationDefinitions = internalOperationDefinitions as GenericOpera * (e.g. `import { termsOperation } from './operations/definitions'`). This map is * intended to be used in situations where the operation type is not known during compile time. */ -export const operationDefinitionMap = internalOperationDefinitions.reduce( +export const operationDefinitionMap: Record< + string, + GenericOperationDefinition +> = internalOperationDefinitions.reduce( (definitionMap, definition) => ({ ...definitionMap, [definition.type]: definition }), {} -) as Record; +); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index 4c37d95f6b050..c02f7bcb7d2cd 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -6,11 +6,12 @@ import { i18n } from '@kbn/i18n'; import { OperationDefinition } from './index'; -import { FormattedIndexPatternColumn } from './column_types'; +import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types'; -type MetricColumn = FormattedIndexPatternColumn & { - operationType: T; -}; +type MetricColumn = FormattedIndexPatternColumn & + FieldBasedIndexPatternColumn & { + operationType: T; + }; function buildMetricOperation>({ type, @@ -27,6 +28,7 @@ function buildMetricOperation>({ type, priority, displayName, + input: 'field', getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type: fieldType }) => { if ( fieldType === 'number' && @@ -78,7 +80,7 @@ function buildMetricOperation>({ missing: 0, }, }), - } as OperationDefinition; + } as OperationDefinition; } export type SumIndexPatternColumn = MetricColumn<'sum'>; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx index 530c2e962759b..1971fb2875bed 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx @@ -76,12 +76,13 @@ function getEsAggsParams({ sourceField, params }: RangeIndexPatternColumn) { }; } -export const rangeOperation: OperationDefinition = { +export const rangeOperation: OperationDefinition = { type: 'range', displayName: i18n.translate('xpack.lens.indexPattern.ranges', { defaultMessage: 'Ranges', }), priority: 4, // Higher than terms, so numbers get histogram + input: 'field', getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { if ( type === 'number' && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx index c1a87a2013747..c147029bbd3c7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx @@ -48,12 +48,13 @@ export interface TermsIndexPatternColumn extends FieldBasedIndexPatternColumn { }; } -export const termsOperation: OperationDefinition = { +export const termsOperation: OperationDefinition = { type: 'terms', displayName: i18n.translate('xpack.lens.indexPattern.terms', { defaultMessage: 'Top values', }), priority: 3, // Higher than any metric + input: 'field', getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { if ( supportedTypes.has(type) && @@ -95,23 +96,25 @@ export const termsOperation: OperationDefinition = { }, }; }, - toEsAggsConfig: (column, columnId, _indexPattern) => ({ - id: columnId, - enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: column.sourceField, - orderBy: - column.params.orderBy.type === 'alphabetical' ? '_key' : column.params.orderBy.columnId, - order: column.params.orderDirection, - size: column.params.size, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - }, - }), + toEsAggsConfig: (column, columnId, _indexPattern) => { + return { + id: columnId, + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: column.sourceField, + orderBy: + column.params.orderBy.type === 'alphabetical' ? '_key' : column.params.orderBy.columnId, + order: column.params.orderDirection, + size: column.params.size, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }; + }, onFieldChange: (oldColumn, indexPattern, field) => { return { ...oldColumn, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts index 1e2bc5dcb6b62..31a36c59274da 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts @@ -5,4 +5,4 @@ */ export * from './operations'; -export { OperationType, IndexPatternColumn } from './definitions'; +export { OperationType, IndexPatternColumn, FieldBasedIndexPatternColumn } from './definitions'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 703431f724c5d..c1bd4b84099b7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -182,7 +182,7 @@ describe('getOperationTypesForField', () => { }, }; - it('should build a column for the given operation type if it is passed in', () => { + it('should build a column for the given field-based operation type if it is passed in', () => { const column = buildColumn({ layerId: 'first', indexPattern: expectedIndexPatterns[1], @@ -194,6 +194,17 @@ describe('getOperationTypesForField', () => { expect(column.operationType).toEqual('count'); }); + it('should build a column for the given no-input operation type if it is passed in', () => { + const column = buildColumn({ + layerId: 'first', + indexPattern: expectedIndexPatterns[1], + columns: state.layers.first.columns, + suggestedPriority: 0, + op: 'filters', + }); + expect(column.operationType).toEqual('filters'); + }); + it('should build a column for the given operation type and field if it is passed in', () => { const field = expectedIndexPatterns[1].fields[1]; const column = buildColumn({ @@ -222,7 +233,7 @@ describe('getOperationTypesForField', () => { ); }); - it('should list out all field-operation tuples for different operation meta data', () => { + it('should list out all operation tuples', () => { expect(getAvailableOperationsByMetadata(expectedIndexPatterns[1])).toMatchInlineSnapshot(` Array [ Object { @@ -255,13 +266,17 @@ describe('getOperationTypesForField', () => { }, Object { "operationMetaData": Object { - "dataType": "number", + "dataType": "string", "isBucketed": true, "scale": "ordinal", }, "operations": Array [ Object { - "field": "bytes", + "operationType": "filters", + "type": "none", + }, + Object { + "field": "source", "operationType": "terms", "type": "field", }, @@ -269,13 +284,13 @@ describe('getOperationTypesForField', () => { }, Object { "operationMetaData": Object { - "dataType": "string", + "dataType": "number", "isBucketed": true, "scale": "ordinal", }, "operations": Array [ Object { - "field": "source", + "field": "bytes", "operationType": "terms", "type": "field", }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts index 9e5a0f496357d..46dd73ba849a2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts @@ -63,7 +63,7 @@ export function getOperationTypesForField(field: IndexPatternField): OperationTy return operationDefinitions .filter( (operationDefinition) => - 'getPossibleOperationForField' in operationDefinition && + operationDefinition.input === 'field' && operationDefinition.getPossibleOperationForField(field) ) .sort(getSortScoreByPriority) @@ -80,11 +80,16 @@ export function isDocumentOperation(type: string) { return documentOperations.has(type); } -interface OperationFieldTuple { - type: 'field'; - operationType: OperationType; - field: string; -} +type OperationFieldTuple = + | { + type: 'field'; + operationType: OperationType; + field: string; + } + | { + type: 'none'; + operationType: OperationType; + }; /** * Returns all possible operations (matches between operations and fields of the index @@ -100,11 +105,18 @@ interface OperationFieldTuple { * [ * { * operationMetaData: { dataType: 'string', isBucketed: true }, - * operations: ['terms'] + * operations: [{ + * type: 'field', + * operationType: ['terms'], + * field: 'keyword' + * }] * }, * { - * operationMetaData: { dataType: 'number', isBucketed: false }, - * operations: ['avg', 'min', 'max'] + * operationMetaData: { dataType: 'string', isBucketed: true }, + * operations: [{ + * type: 'none', + * operationType: ['filters'], + * }] * }, * ] * ``` @@ -133,30 +145,31 @@ export function getAvailableOperationsByMetadata(indexPattern: IndexPattern) { }; operationDefinitions.sort(getSortScoreByPriority).forEach((operationDefinition) => { - indexPattern.fields.forEach((field) => { + if (operationDefinition.input === 'field') { + indexPattern.fields.forEach((field) => { + addToMap( + { + type: 'field', + operationType: operationDefinition.type, + field: field.name, + }, + operationDefinition.getPossibleOperationForField(field) + ); + }); + } else if (operationDefinition.input === 'none') { addToMap( { - type: 'field', + type: 'none', operationType: operationDefinition.type, - field: field.name, }, - getPossibleOperationForField(operationDefinition, field) + operationDefinition.getPossibleOperation() ); - }); + } }); return Object.values(operationByMetadata); } -function getPossibleOperationForField( - operationDefinition: GenericOperationDefinition, - field: IndexPatternField -): OperationMetadata | undefined { - return 'getPossibleOperationForField' in operationDefinition - ? operationDefinition.getPossibleOperationForField(field) - : undefined; -} - /** * Changes the field of the passed in colum. To do so, this method uses the `onFieldChange` function of * the operation definition of the column. Returns a new column object with the field changed. @@ -171,13 +184,13 @@ export function changeField( ) { const operationDefinition = operationDefinitionMap[column.operationType]; - if (!('onFieldChange' in operationDefinition)) { + if (operationDefinition.input === 'field' && 'sourceField' in column) { + return operationDefinition.onFieldChange(column, indexPattern, newField); + } else { throw new Error( "Invariant error: Cannot change field if operation isn't a field based operaiton" ); } - - return operationDefinition.onFieldChange(column, indexPattern, newField); } /** @@ -203,7 +216,7 @@ export function buildColumn({ suggestedPriority: DimensionPriority | undefined; layerId: string; indexPattern: IndexPattern; - field: IndexPatternField; + field?: IndexPatternField; previousColumn?: IndexPatternColumn; }): IndexPatternColumn { const operationDefinition = operationDefinitionMap[op]; @@ -220,16 +233,18 @@ export function buildColumn({ previousColumn, }; + if (operationDefinition.input === 'none') { + return operationDefinition.buildColumn(baseOptions); + } + if (!field) { throw new Error(`Invariant error: ${operationDefinition.type} operation requires field`); } - const newColumn = operationDefinition.buildColumn({ + return operationDefinition.buildColumn({ ...baseOptions, field, }); - - return newColumn; } export { operationDefinitionMap } from './definitions'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts index 51691ae18a99a..c977a7e0fa370 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts @@ -5,8 +5,7 @@ */ import _ from 'lodash'; -import { isColumnTransferable } from './operations'; -import { operationDefinitionMap, IndexPatternColumn } from './operations'; +import { isColumnTransferable, operationDefinitionMap, IndexPatternColumn } from './operations'; import { IndexPattern, IndexPatternPrivateState, IndexPatternLayer } from './types'; export function updateColumnParam({ diff --git a/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts new file mode 100644 index 0000000000000..8bf078806cfbc --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts @@ -0,0 +1,117 @@ +/* + * 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 uuid from 'uuid/v4'; +import { + AggDescriptor, + ColorDynamicOptions, + LayerDescriptor, +} from '../../../common/descriptor_types'; +import { + AGG_TYPE, + COLOR_MAP_TYPE, + FIELD_ORIGIN, + SOURCE_TYPES, + STYLE_TYPE, + VECTOR_STYLES, +} from '../../../common/constants'; +import { VectorStyle } from '../styles/vector/vector_style'; +import { EMSFileSource } from '../sources/ems_file_source'; +// @ts-ignore +import { ESGeoGridSource } from '../sources/es_geo_grid_source'; +import { VectorLayer } from './vector_layer/vector_layer'; +import { getDefaultDynamicProperties } from '../styles/vector/vector_style_defaults'; +import { NUMERICAL_COLOR_PALETTES } from '../styles/color_palettes'; +import { getJoinAggKey } from '../../../common/get_agg_key'; + +const defaultDynamicProperties = getDefaultDynamicProperties(); + +export function createAggDescriptor(metricAgg: string, metricFieldName?: string): AggDescriptor { + const aggTypeKey = Object.keys(AGG_TYPE).find((key) => { + return AGG_TYPE[key as keyof typeof AGG_TYPE] === metricAgg; + }); + const aggType = aggTypeKey ? AGG_TYPE[aggTypeKey as keyof typeof AGG_TYPE] : undefined; + + return aggType && metricFieldName + ? { type: aggType, field: metricFieldName } + : { type: AGG_TYPE.COUNT }; +} + +export function createRegionMapLayerDescriptor({ + label, + emsLayerId, + leftFieldName, + termsFieldName, + colorSchema, + indexPatternId, + indexPatternTitle, + metricAgg, + metricFieldName, +}: { + label: string; + emsLayerId?: string; + leftFieldName?: string; + termsFieldName?: string; + colorSchema: string; + indexPatternId?: string; + indexPatternTitle?: string; + metricAgg: string; + metricFieldName?: string; +}): LayerDescriptor | null { + if (!indexPatternId || !emsLayerId || !leftFieldName || !termsFieldName) { + return null; + } + + const metricsDescriptor = createAggDescriptor(metricAgg, metricFieldName); + const joinId = uuid(); + const joinKey = getJoinAggKey({ + aggType: metricsDescriptor.type, + aggFieldName: metricsDescriptor.field ? metricsDescriptor.field : '', + rightSourceId: joinId, + }); + const colorPallette = NUMERICAL_COLOR_PALETTES.find((pallette) => { + return pallette.value.toLowerCase() === colorSchema.toLowerCase(); + }); + return VectorLayer.createDescriptor({ + label, + joins: [ + { + leftField: leftFieldName, + right: { + type: SOURCE_TYPES.ES_TERM_SOURCE, + id: joinId, + indexPatternId, + indexPatternTitle: indexPatternTitle ? indexPatternTitle : indexPatternId, + term: termsFieldName, + metrics: [metricsDescriptor], + }, + }, + ], + sourceDescriptor: EMSFileSource.createDescriptor({ + id: emsLayerId, + tooltipProperties: ['name', leftFieldName], + }), + style: VectorStyle.createDescriptor({ + [VECTOR_STYLES.FILL_COLOR]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR]!.options as ColorDynamicOptions), + field: { + name: joinKey, + origin: FIELD_ORIGIN.JOIN, + }, + color: colorPallette ? colorPallette.value : 'Yellow to Red', + type: COLOR_MAP_TYPE.ORDINAL, + fieldMetaOptions: { + ...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR]!.options as ColorDynamicOptions) + .fieldMetaOptions, + isEnabled: false, + }, + }, + }, + }), + }); +} diff --git a/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.test.ts b/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.test.ts new file mode 100644 index 0000000000000..18e5f462bb310 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.test.ts @@ -0,0 +1,43 @@ +/* + * 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 { createAggDescriptor } from './create_tile_map_layer_descriptor'; + +describe('createAggDescriptor', () => { + test('Should allow supported metric aggs', () => { + expect(createAggDescriptor('Scaled Circle Markers', 'sum', 'bytes')).toEqual({ + type: 'sum', + field: 'bytes', + }); + }); + + test('Should fallback to count when field not provided', () => { + expect(createAggDescriptor('Scaled Circle Markers', 'sum', undefined)).toEqual({ + type: 'count', + }); + }); + + test('Should fallback to count when metric agg is not supported in maps', () => { + expect(createAggDescriptor('Scaled Circle Markers', 'top_hits', 'bytes')).toEqual({ + type: 'count', + }); + }); + + describe('heatmap', () => { + test('Should allow countable metric aggs', () => { + expect(createAggDescriptor('Heatmap', 'sum', 'bytes')).toEqual({ + type: 'sum', + field: 'bytes', + }); + }); + + test('Should fallback to count for non-countable metric aggs', () => { + expect(createAggDescriptor('Heatmap', 'avg', 'bytes')).toEqual({ + type: 'count', + }); + }); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts new file mode 100644 index 0000000000000..05a8620e436d5 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts @@ -0,0 +1,159 @@ +/* + * 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 { + AggDescriptor, + ColorDynamicOptions, + LayerDescriptor, + SizeDynamicOptions, + VectorStylePropertiesDescriptor, +} from '../../../common/descriptor_types'; +import { + AGG_TYPE, + COLOR_MAP_TYPE, + FIELD_ORIGIN, + GRID_RESOLUTION, + RENDER_AS, + STYLE_TYPE, + VECTOR_STYLES, +} from '../../../common/constants'; +import { VectorStyle } from '../styles/vector/vector_style'; +// @ts-ignore +import { ESGeoGridSource } from '../sources/es_geo_grid_source'; +import { VectorLayer } from './vector_layer/vector_layer'; +// @ts-ignore +import { HeatmapLayer } from './heatmap_layer/heatmap_layer'; +import { getDefaultDynamicProperties } from '../styles/vector/vector_style_defaults'; +import { NUMERICAL_COLOR_PALETTES } from '../styles/color_palettes'; +import { getSourceAggKey } from '../../../common/get_agg_key'; +import { isMetricCountable } from '../util/is_metric_countable'; + +const defaultDynamicProperties = getDefaultDynamicProperties(); + +function isHeatmap(mapType: string): boolean { + return mapType.toLowerCase() === 'heatmap'; +} + +function getGeoGridRequestType(mapType: string): RENDER_AS { + if (isHeatmap(mapType)) { + return RENDER_AS.HEATMAP; + } + + if (mapType.toLowerCase() === 'shaded geohash grid') { + return RENDER_AS.GRID; + } + + return RENDER_AS.POINT; +} + +export function createAggDescriptor( + mapType: string, + metricAgg: string, + metricFieldName?: string +): AggDescriptor { + const aggTypeKey = Object.keys(AGG_TYPE).find((key) => { + return AGG_TYPE[key as keyof typeof AGG_TYPE] === metricAgg; + }); + const aggType = aggTypeKey ? AGG_TYPE[aggTypeKey as keyof typeof AGG_TYPE] : undefined; + + return aggType && metricFieldName && (!isHeatmap(mapType) || isMetricCountable(aggType)) + ? { type: aggType, field: metricFieldName } + : { type: AGG_TYPE.COUNT }; +} + +export function createTileMapLayerDescriptor({ + label, + mapType, + colorSchema, + indexPatternId, + geoFieldName, + metricAgg, + metricFieldName, +}: { + label: string; + mapType: string; + colorSchema: string; + indexPatternId?: string; + geoFieldName?: string; + metricAgg: string; + metricFieldName?: string; +}): LayerDescriptor | null { + if (!indexPatternId || !geoFieldName) { + return null; + } + + const metricsDescriptor = createAggDescriptor(mapType, metricAgg, metricFieldName); + const geoGridSourceDescriptor = ESGeoGridSource.createDescriptor({ + indexPatternId, + geoField: geoFieldName, + metrics: [metricsDescriptor], + requestType: getGeoGridRequestType(mapType), + resolution: GRID_RESOLUTION.MOST_FINE, + }); + + if (isHeatmap(mapType)) { + return HeatmapLayer.createDescriptor({ + label, + sourceDescriptor: geoGridSourceDescriptor, + }); + } + + const metricSourceKey = getSourceAggKey({ + aggType: metricsDescriptor.type, + aggFieldName: metricsDescriptor.field, + }); + const metricStyleField = { + name: metricSourceKey, + origin: FIELD_ORIGIN.SOURCE, + }; + + const colorPallette = NUMERICAL_COLOR_PALETTES.find((pallette) => { + return pallette.value.toLowerCase() === colorSchema.toLowerCase(); + }); + const styleProperties: VectorStylePropertiesDescriptor = { + [VECTOR_STYLES.FILL_COLOR]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR]!.options as ColorDynamicOptions), + field: metricStyleField, + color: colorPallette ? colorPallette.value : 'Yellow to Red', + type: COLOR_MAP_TYPE.ORDINAL, + fieldMetaOptions: { + ...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR]!.options as ColorDynamicOptions) + .fieldMetaOptions, + isEnabled: false, + }, + }, + }, + [VECTOR_STYLES.LINE_COLOR]: { + type: STYLE_TYPE.STATIC, + options: { + color: '#3d3d3d', + }, + }, + }; + if (mapType.toLowerCase() === 'scaled circle markers') { + styleProperties[VECTOR_STYLES.ICON_SIZE] = { + type: STYLE_TYPE.DYNAMIC, + options: { + ...(defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE]!.options as SizeDynamicOptions), + maxSize: 18, + field: metricStyleField, + fieldMetaOptions: { + ...(defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE]!.options as SizeDynamicOptions) + .fieldMetaOptions, + isEnabled: false, + }, + }, + }; + } + + return VectorLayer.createDescriptor({ + label, + sourceDescriptor: geoGridSourceDescriptor, + style: VectorStyle.createDescriptor(styleProperties), + }); +} diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts index 85601cfc17e8f..bdd86d78b5300 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts @@ -29,7 +29,6 @@ import { OBSERVABILITY_LAYER_TYPE } from './layer_select'; import { OBSERVABILITY_METRIC_TYPE } from './metric_select'; import { DISPLAY } from './display_select'; import { VectorStyle } from '../../../styles/vector/vector_style'; -// @ts-ignore import { EMSFileSource } from '../../../sources/ems_file_source'; // @ts-ignore import { ESGeoGridSource } from '../../../sources/es_geo_grid_source'; diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx index 5f73a9e23431b..38e13a68437c7 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx @@ -72,9 +72,7 @@ export class EMSFileSource extends AbstractVectorSource implements IEmsFileSourc async getEMSFileLayer(): Promise { const emsFileLayers = await getEmsFileLayers(); - const emsFileLayer = emsFileLayers.find( - (fileLayer) => fileLayer.getId() === this._descriptor.id - ); + const emsFileLayer = emsFileLayers.find((fileLayer) => fileLayer.hasId(this._descriptor.id)); if (!emsFileLayer) { throw new Error( i18n.translate('xpack.maps.source.emsFile.unableToFindIdErrorMessage', { diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts index 03752a1c3e11e..9bced75b613d7 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts @@ -48,6 +48,44 @@ interface LazyLoadedMapModules { registerLayerWizard: (layerWizard: LayerWizard) => void; registerSource(entry: SourceRegistryEntry): void; getIndexPatternsFromIds: (indexPatternIds: string[]) => Promise; + createTileMapLayerDescriptor: ({ + label, + mapType, + colorSchema, + indexPatternId, + geoFieldName, + metricAgg, + metricFieldName, + }: { + label: string; + mapType: string; + colorSchema: string; + indexPatternId?: string; + geoFieldName?: string; + metricAgg: string; + metricFieldName?: string; + }) => LayerDescriptor | null; + createRegionMapLayerDescriptor: ({ + label, + emsLayerId, + leftFieldName, + termsFieldName, + colorSchema, + indexPatternId, + indexPatternTitle, + metricAgg, + metricFieldName, + }: { + label: string; + emsLayerId?: string; + leftFieldName?: string; + termsFieldName?: string; + colorSchema: string; + indexPatternId?: string; + indexPatternTitle?: string; + metricAgg: string; + metricFieldName?: string; + }) => LayerDescriptor | null; } export async function lazyLoadMapModules(): Promise { @@ -72,6 +110,8 @@ export async function lazyLoadMapModules(): Promise { registerLayerWizard, registerSource, getIndexPatternsFromIds, + createTileMapLayerDescriptor, + createRegionMapLayerDescriptor, } = await import('./lazy'); resolve({ @@ -90,6 +130,8 @@ export async function lazyLoadMapModules(): Promise { registerLayerWizard, registerSource, getIndexPatternsFromIds, + createTileMapLayerDescriptor, + createRegionMapLayerDescriptor, }); }); return loadModulesPromise; diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts index 28f5acdc17656..782d645dc230a 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts @@ -20,3 +20,5 @@ export * from '../../classes/layers/solution_layers/security'; export { registerLayerWizard } from '../../classes/layers/layer_wizard_registry'; export { registerSource } from '../../classes/sources/source_registry'; export { getIndexPatternsFromIds } from '../../index_pattern_util'; +export { createTileMapLayerDescriptor } from '../../classes/layers/create_tile_map_layer_descriptor'; +export { createRegionMapLayerDescriptor } from '../../classes/layers/create_region_map_layer_descriptor'; diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 8f49598cf2a8d..696964f0258d4 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -32,7 +32,11 @@ import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { VisualizationsSetup } from '../../../../src/plugins/visualizations/public'; import { APP_ICON_SOLUTION, APP_ID, MAP_SAVED_OBJECT_TYPE } from '../common/constants'; import { VISUALIZE_GEO_FIELD_TRIGGER } from '../../../../src/plugins/ui_actions/public'; -import { createMapsUrlGenerator } from './url_generator'; +import { + createMapsUrlGenerator, + createRegionMapUrlGenerator, + createTileMapUrlGenerator, +} from './url_generator'; import { visualizeGeoFieldAction } from './trigger_actions/visualize_geo_field_action'; import { MapEmbeddableFactory } from './embeddable/map_embeddable_factory'; import { EmbeddableSetup } from '../../../../src/plugins/embeddable/public'; @@ -97,15 +101,18 @@ export class MapsPlugin setKibanaCommonConfig(plugins.mapsLegacy.config); setMapAppConfig(config); setKibanaVersion(this._initializerContext.env.packageInfo.version); - plugins.share.urlGenerators.registerUrlGenerator( - createMapsUrlGenerator(async () => { - const [coreStart] = await core.getStartServices(); - return { - appBasePath: coreStart.application.getUrlForApp('maps'), - useHashedUrl: coreStart.uiSettings.get('state:storeInSessionStorage'), - }; - }) - ); + + // register url generators + const getStartServices = async () => { + const [coreStart] = await core.getStartServices(); + return { + appBasePath: coreStart.application.getUrlForApp('maps'), + useHashedUrl: coreStart.uiSettings.get('state:storeInSessionStorage'), + }; + }; + plugins.share.urlGenerators.registerUrlGenerator(createMapsUrlGenerator(getStartServices)); + plugins.share.urlGenerators.registerUrlGenerator(createTileMapUrlGenerator(getStartServices)); + plugins.share.urlGenerators.registerUrlGenerator(createRegionMapUrlGenerator(getStartServices)); plugins.inspector.registerView(MapView); if (plugins.home) { diff --git a/x-pack/plugins/maps/public/url_generator.test.ts b/x-pack/plugins/maps/public/url_generator.test.ts index a44f8d952fde1..880d5a5e03b43 100644 --- a/x-pack/plugins/maps/public/url_generator.test.ts +++ b/x-pack/plugins/maps/public/url_generator.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import rison from 'rison-node'; + import { createMapsUrlGenerator } from './url_generator'; import { LAYER_TYPE, SOURCE_TYPES, SCALING_TYPES } from '../common/constants'; import { esFilters } from '../../../../src/plugins/data/public'; @@ -63,12 +63,11 @@ describe('visualize url generator', () => { }, }, ]; - const encodedLayers = rison.encode_array(initialLayers); const url = await generator.createUrl!({ initialLayers, }); expect(url).toMatchInlineSnapshot( - `"test/app/maps/map#/?_g=()&_a=()&initialLayers=${encodedLayers}"` + `"test/app/maps/map#/?_g=()&_a=()&initialLayers=(id%3A'13823000-99b9-11ea-9eb6-d9e8adceb647'%2CsourceDescriptor%3A(geoField%3Atest%2Cid%3A'13823000-99b9-11ea-9eb6-d9e8adceb647'%2CindexPatternId%3A'90943e30-9a47-11e8-b64d-95841ca0b247'%2Clabel%3A'Sample%20Data'%2CscalingType%3ALIMIT%2CtooltipProperties%3A!()%2Ctype%3AES_SEARCH)%2Ctype%3AVECTOR%2Cvisible%3A!t)"` ); }); diff --git a/x-pack/plugins/maps/public/url_generator.ts b/x-pack/plugins/maps/public/url_generator.ts index 3fbb361342c7a..7f7f3f2c60327 100644 --- a/x-pack/plugins/maps/public/url_generator.ts +++ b/x-pack/plugins/maps/public/url_generator.ts @@ -16,11 +16,14 @@ import { setStateToKbnUrl } from '../../../../src/plugins/kibana_utils/public'; import { UrlGeneratorsDefinition } from '../../../../src/plugins/share/public'; import { LayerDescriptor } from '../common/descriptor_types'; import { INITIAL_LAYERS_KEY } from '../common/constants'; +import { lazyLoadMapModules } from './lazy_load_bundle'; const STATE_STORAGE_KEY = '_a'; const GLOBAL_STATE_STORAGE_KEY = '_g'; export const MAPS_APP_URL_GENERATOR = 'MAPS_APP_URL_GENERATOR'; +export const MAPS_APP_TILE_MAP_URL_GENERATOR = 'MAPS_APP_TILE_MAP_URL_GENERATOR'; +export const MAPS_APP_REGION_MAP_URL_GENERATOR = 'MAPS_APP_REGION_MAP_URL_GENERATOR'; export interface MapsUrlGeneratorState { /** @@ -59,51 +62,175 @@ export interface MapsUrlGeneratorState { hash?: boolean; } +type GetStartServices = () => Promise<{ + appBasePath: string; + useHashedUrl: boolean; +}>; + +async function createMapUrl({ + getStartServices, + mapId, + filters, + query, + refreshInterval, + timeRange, + initialLayers, + hash, +}: MapsUrlGeneratorState & { getStartServices: GetStartServices }): Promise { + const startServices = await getStartServices(); + const useHash = hash ?? startServices.useHashedUrl; + const appBasePath = startServices.appBasePath; + + const appState: { + query?: Query; + filters?: Filter[]; + vis?: unknown; + } = {}; + const queryState: QueryState = {}; + + if (query) appState.query = query; + if (filters && filters.length) + appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f)); + + if (timeRange) queryState.time = timeRange; + if (filters && filters.length) + queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f)); + if (refreshInterval) queryState.refreshInterval = refreshInterval; + + let url = `${appBasePath}/map#/${mapId || ''}`; + url = setStateToKbnUrl(GLOBAL_STATE_STORAGE_KEY, queryState, { useHash }, url); + url = setStateToKbnUrl(STATE_STORAGE_KEY, appState, { useHash }, url); + + if (initialLayers && initialLayers.length) { + // @ts-ignore + const risonEncodedInitialLayers = rison.encode_array(initialLayers); + url = `${url}&${INITIAL_LAYERS_KEY}=${encodeURIComponent(risonEncodedInitialLayers)}`; + } + + return url; +} + export const createMapsUrlGenerator = ( - getStartServices: () => Promise<{ - appBasePath: string; - useHashedUrl: boolean; - }> + getStartServices: GetStartServices ): UrlGeneratorsDefinition => ({ id: MAPS_APP_URL_GENERATOR, + createUrl: async (mapsUrlGeneratorState: MapsUrlGeneratorState): Promise => { + return createMapUrl({ ...mapsUrlGeneratorState, getStartServices }); + }, +}); + +export const createTileMapUrlGenerator = ( + getStartServices: GetStartServices +): UrlGeneratorsDefinition => ({ + id: MAPS_APP_TILE_MAP_URL_GENERATOR, + createUrl: async ({ + label, + mapType, + colorSchema, + indexPatternId, + geoFieldName, + metricAgg, + metricFieldName, + filters, + query, + timeRange, + hash, + }: { + label: string; + mapType: string; + colorSchema: string; + indexPatternId?: string; + geoFieldName?: string; + metricAgg: string; + metricFieldName?: string; + timeRange?: TimeRange; + filters?: Filter[]; + query?: Query; + hash?: boolean; + }): Promise => { + const mapModules = await lazyLoadMapModules(); + const initialLayers = []; + const tileMapLayerDescriptor = mapModules.createTileMapLayerDescriptor({ + label, + mapType, + colorSchema, + indexPatternId, + geoFieldName, + metricAgg, + metricFieldName, + }); + if (tileMapLayerDescriptor) { + initialLayers.push(tileMapLayerDescriptor); + } + + return createMapUrl({ + initialLayers, + filters, + query, + timeRange, + hash: true, + getStartServices, + }); + }, +}); + +export const createRegionMapUrlGenerator = ( + getStartServices: GetStartServices +): UrlGeneratorsDefinition => ({ + id: MAPS_APP_REGION_MAP_URL_GENERATOR, createUrl: async ({ - mapId, + label, + emsLayerId, + leftFieldName, + termsFieldName, + colorSchema, + indexPatternId, + indexPatternTitle, + metricAgg, + metricFieldName, filters, query, - refreshInterval, timeRange, - initialLayers, hash, - }: MapsUrlGeneratorState): Promise => { - const startServices = await getStartServices(); - const useHash = hash ?? startServices.useHashedUrl; - const appBasePath = startServices.appBasePath; - - const appState: { - query?: Query; - filters?: Filter[]; - vis?: unknown; - } = {}; - const queryState: QueryState = {}; - - if (query) appState.query = query; - if (filters && filters.length) - appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f)); - - if (timeRange) queryState.time = timeRange; - if (filters && filters.length) - queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f)); - if (refreshInterval) queryState.refreshInterval = refreshInterval; - - let url = `${appBasePath}/map#/${mapId || ''}`; - url = setStateToKbnUrl(GLOBAL_STATE_STORAGE_KEY, queryState, { useHash }, url); - url = setStateToKbnUrl(STATE_STORAGE_KEY, appState, { useHash }, url); - - if (initialLayers && initialLayers.length) { - // @ts-ignore - url = `${url}&${INITIAL_LAYERS_KEY}=${rison.encode_array(initialLayers)}`; + }: { + label: string; + emsLayerId?: string; + leftFieldName?: string; + termsFieldName?: string; + colorSchema: string; + indexPatternId?: string; + indexPatternTitle?: string; + metricAgg: string; + metricFieldName?: string; + timeRange?: TimeRange; + filters?: Filter[]; + query?: Query; + hash?: boolean; + }): Promise => { + const mapModules = await lazyLoadMapModules(); + const initialLayers = []; + const regionMapLayerDescriptor = mapModules.createRegionMapLayerDescriptor({ + label, + emsLayerId, + leftFieldName, + termsFieldName, + colorSchema, + indexPatternId, + indexPatternTitle, + metricAgg, + metricFieldName, + }); + if (regionMapLayerDescriptor) { + initialLayers.push(regionMapLayerDescriptor); } - return url; + return createMapUrl({ + initialLayers, + filters, + query, + timeRange, + hash: true, + getStartServices, + }); }, }); diff --git a/x-pack/plugins/ml/common/types/ml_url_generator.ts b/x-pack/plugins/ml/common/types/ml_url_generator.ts index d176c22bdbb62..95d06e62f9ef0 100644 --- a/x-pack/plugins/ml/common/types/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/types/ml_url_generator.ts @@ -167,6 +167,7 @@ export interface DataFrameAnalyticsExplorationQueryState { ml: { jobId: JobId; analysisType: DataFrameAnalysisConfigType; + defaultIsTraining?: boolean; }; } @@ -176,6 +177,7 @@ export type DataFrameAnalyticsExplorationUrlState = MLPageState< jobId: JobId; analysisType: DataFrameAnalysisConfigType; globalState?: MlCommonGlobalState; + defaultIsTraining?: boolean; } >; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index d22bba7738db4..49f3f2311a938 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -67,6 +67,17 @@ export const defaultSearchQuery = { match_all: {}, }; +export const getDefaultTrainingFilterQuery = (resultsField: string, isTraining: boolean) => ({ + bool: { + minimum_should_match: 1, + should: [ + { + match: { [`${resultsField}.is_training`]: isTraining }, + }, + ], + }, +}); + export interface SearchQuery { track_total_hits?: boolean; query: SavedSearchQuery; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts index 83eebccd310e3..7ba3e910ddd32 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -8,6 +8,7 @@ export { getAnalysisType, getDependentVar, getPredictionFieldName, + getDefaultTrainingFilterQuery, isOutlierAnalysis, refreshAnalyticsList$, useRefreshAnalyticsList, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx index 9fe5963593229..bfa63e21e6c94 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx @@ -82,7 +82,11 @@ export const CreateStepFooter: FC = ({ jobId, jobType, showProgress }) => jobStats.state === DATA_FRAME_TASK_STATE.STOPPED ) { clearInterval(interval); - setJobFinished(true); + // Check job has started. Jobs that fail to start will also have STOPPED state + setJobFinished( + progressStats.currentPhase === progressStats.totalPhases && + progressStats.progress === 100 + ); } } else { clearInterval(interval); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx index 2e3a5d89367ce..28ef3898cde97 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx @@ -13,20 +13,20 @@ import { EvaluatePanel } from './evaluate_panel'; interface Props { jobId: string; + defaultIsTraining?: boolean; } -export const ClassificationExploration: FC = ({ jobId }) => { - return ( - - ); -}; +export const ClassificationExploration: FC = ({ jobId, defaultIsTraining }) => ( + +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx index 84b44ef0d349f..f3fc65d264e62 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx @@ -27,9 +27,15 @@ interface Props { jobId: string; title: string; EvaluatePanel: FC; + defaultIsTraining?: boolean; } -export const ExplorationPageWrapper: FC = ({ jobId, title, EvaluatePanel }) => { +export const ExplorationPageWrapper: FC = ({ + jobId, + title, + EvaluatePanel, + defaultIsTraining, +}) => { const { indexPattern, isInitialized, @@ -70,6 +76,7 @@ export const ExplorationPageWrapper: FC = ({ jobId, title, EvaluatePanel needsDestIndexPattern={needsDestIndexPattern} setEvaluateSearchQuery={setSearchQuery} title={title} + defaultIsTraining={defaultIsTraining} /> )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx index 8c158c1ca14a0..8ed732bf7da2b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx @@ -52,7 +52,7 @@ export const ExplorationQueryBar: FC = ({ if (defaultQueryString !== undefined) { setSearchInput({ query: defaultQueryString, language: SEARCH_QUERY_LANGUAGE.KUERY }); } - }, []); + }, [defaultQueryString !== undefined]); const searchChangeHandler = (query: Query) => setSearchInput(query); const searchSubmitHandler = (query: Query) => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx index 07a15b01fca93..ef014b07a937e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx @@ -20,6 +20,7 @@ import { SEARCH_SIZE, defaultSearchQuery, getAnalysisType, + getDefaultTrainingFilterQuery, } from '../../../../common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/use_columns'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; @@ -30,6 +31,7 @@ import { IndexPatternPrompt } from '../index_pattern_prompt'; import { useExplorationResults } from './use_exploration_results'; import { useMlKibana } from '../../../../../contexts/kibana'; import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; +import { useUrlState } from '../../../../../util/url_state'; const showingDocs = i18n.translate( 'xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText', @@ -53,6 +55,7 @@ interface Props { needsDestIndexPattern: boolean; setEvaluateSearchQuery: React.Dispatch>; title: string; + defaultIsTraining?: boolean; } export const ExplorationResultsTable: FC = React.memo( @@ -63,18 +66,36 @@ export const ExplorationResultsTable: FC = React.memo( needsDestIndexPattern, setEvaluateSearchQuery, title, + defaultIsTraining, }) => { const { services: { mlServices: { mlApiServices }, }, } = useMlKibana(); + const [globalState, setGlobalState] = useUrlState('_g'); const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + const [defaultQueryString, setDefaultQueryString] = useState(); useEffect(() => { setEvaluateSearchQuery(searchQuery); }, [JSON.stringify(searchQuery)]); + useEffect(() => { + if (defaultIsTraining !== undefined) { + // Apply defaultIsTraining filter + setSearchQuery( + getDefaultTrainingFilterQuery(jobConfig.dest.results_field, defaultIsTraining) + ); + setDefaultQueryString(`${jobConfig.dest.results_field}.is_training : ${defaultIsTraining}`); + // Clear defaultIsTraining from url + setGlobalState('ml', { + analysisType: globalState.ml.analysisType, + jobId: globalState.ml.jobId, + }); + } + }, []); + const analysisType = getAnalysisType(jobConfig.analysis); const classificationData = useExplorationResults( @@ -140,6 +161,7 @@ export const ExplorationResultsTable: FC = React.memo( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx index 36d91f6f41d44..40279ecc6ffa4 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx @@ -14,17 +14,17 @@ import { EvaluatePanel } from './evaluate_panel'; interface Props { jobId: string; + defaultIsTraining?: boolean; } -export const RegressionExploration: FC = ({ jobId }) => { - return ( - - ); -}; +export const RegressionExploration: FC = ({ jobId, defaultIsTraining }) => ( + +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx index f4f01330271fc..4620bbd969fab 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx @@ -32,7 +32,8 @@ import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_fr export const Page: FC<{ jobId: string; analysisType: DataFrameAnalysisConfigType; -}> = ({ jobId, analysisType }) => ( + defaultIsTraining?: boolean; +}> = ({ jobId, analysisType, defaultIsTraining }) => ( @@ -70,10 +71,10 @@ export const Page: FC<{ )} {analysisType === ANALYSIS_CONFIG_TYPE.REGRESSION && ( - + )} {analysisType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && ( - + )} diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx index f9f2ebe48f4aa..b2d2a92617922 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx @@ -67,10 +67,11 @@ const PageWrapper: FC = ({ location, deps }) => { } const jobId: string = globalState.ml.jobId; const analysisType: DataFrameAnalysisConfigType = globalState.ml.analysisType; + const defaultIsTraining: boolean | undefined = globalState.ml.defaultIsTraining; return ( - + ); }; diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts index 88761edf241a9..2408290e76773 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts @@ -61,12 +61,13 @@ export function createDataFrameAnalyticsExplorationUrl( let url = `${appBasePath}/${ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION}`; if (mlUrlGeneratorState) { - const { jobId, analysisType, globalState } = mlUrlGeneratorState; + const { jobId, analysisType, defaultIsTraining, globalState } = mlUrlGeneratorState; const queryState: DataFrameAnalyticsExplorationQueryState = { ml: { jobId, analysisType, + defaultIsTraining, }, ...globalState, }; diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts index 17caf80eef22b..2f63a878b0cde 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts @@ -80,33 +80,36 @@ describe('getMonitoringUsageCollector', () => { expect(args[0].schema).toStrictEqual({ hasMonitoringData: { type: 'boolean' }, clusters: { - license: { type: 'keyword' }, - clusterUuid: { type: 'keyword' }, - metricbeatUsed: { type: 'boolean' }, - elasticsearch: { - enabled: { type: 'boolean' }, - count: { type: 'long' }, - metricbeatUsed: { type: 'boolean' }, - }, - kibana: { - enabled: { type: 'boolean' }, - count: { type: 'long' }, - metricbeatUsed: { type: 'boolean' }, - }, - logstash: { - enabled: { type: 'boolean' }, - count: { type: 'long' }, - metricbeatUsed: { type: 'boolean' }, - }, - beats: { - enabled: { type: 'boolean' }, - count: { type: 'long' }, - metricbeatUsed: { type: 'boolean' }, - }, - apm: { - enabled: { type: 'boolean' }, - count: { type: 'long' }, + type: 'array', + items: { + license: { type: 'keyword' }, + clusterUuid: { type: 'keyword' }, metricbeatUsed: { type: 'boolean' }, + elasticsearch: { + enabled: { type: 'boolean' }, + count: { type: 'long' }, + metricbeatUsed: { type: 'boolean' }, + }, + kibana: { + enabled: { type: 'boolean' }, + count: { type: 'long' }, + metricbeatUsed: { type: 'boolean' }, + }, + logstash: { + enabled: { type: 'boolean' }, + count: { type: 'long' }, + metricbeatUsed: { type: 'boolean' }, + }, + beats: { + enabled: { type: 'boolean' }, + count: { type: 'long' }, + metricbeatUsed: { type: 'boolean' }, + }, + apm: { + enabled: { type: 'boolean' }, + count: { type: 'long' }, + metricbeatUsed: { type: 'boolean' }, + }, }, }, }); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts index b743a5f8e0b4f..278a6c163c0ad 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts @@ -28,68 +28,71 @@ export function getMonitoringUsageCollector( type: 'boolean', }, clusters: { - license: { - type: 'keyword', - }, - clusterUuid: { - type: 'keyword', - }, - metricbeatUsed: { - type: 'boolean', - }, - elasticsearch: { - enabled: { - type: 'boolean', + type: 'array', + items: { + license: { + type: 'keyword', }, - count: { - type: 'long', + clusterUuid: { + type: 'keyword', }, metricbeatUsed: { type: 'boolean', }, - }, - kibana: { - enabled: { - type: 'boolean', - }, - count: { - type: 'long', + elasticsearch: { + enabled: { + type: 'boolean', + }, + count: { + type: 'long', + }, + metricbeatUsed: { + type: 'boolean', + }, }, - metricbeatUsed: { - type: 'boolean', + kibana: { + enabled: { + type: 'boolean', + }, + count: { + type: 'long', + }, + metricbeatUsed: { + type: 'boolean', + }, }, - }, - logstash: { - enabled: { - type: 'boolean', + logstash: { + enabled: { + type: 'boolean', + }, + count: { + type: 'long', + }, + metricbeatUsed: { + type: 'boolean', + }, }, - count: { - type: 'long', - }, - metricbeatUsed: { - type: 'boolean', + beats: { + enabled: { + type: 'boolean', + }, + count: { + type: 'long', + }, + metricbeatUsed: { + type: 'boolean', + }, }, - }, - beats: { - enabled: { - type: 'boolean', - }, - count: { - type: 'long', - }, - metricbeatUsed: { - type: 'boolean', - }, - }, - apm: { - enabled: { - type: 'boolean', - }, - count: { - type: 'long', - }, - metricbeatUsed: { - type: 'boolean', + apm: { + enabled: { + type: 'boolean', + }, + count: { + type: 'long', + }, + metricbeatUsed: { + type: 'boolean', + }, }, }, }, diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts index 11e58f7f95fc2..90483d7c0a4d5 100644 --- a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts @@ -62,10 +62,16 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens type: 'number', }, enabledAuthProviders: { - type: 'keyword', + type: 'array', + items: { + type: 'keyword', + }, }, httpAuthSchemes: { - type: 'keyword', + type: 'array', + items: { + type: 'keyword', + }, }, }, fetch: () => { diff --git a/x-pack/plugins/security_solution/cypress/cypress.json b/x-pack/plugins/security_solution/cypress/cypress.json index b097b0432e75d..173514565c8bb 100644 --- a/x-pack/plugins/security_solution/cypress/cypress.json +++ b/x-pack/plugins/security_solution/cypress/cypress.json @@ -1,6 +1,9 @@ { "baseUrl": "http://localhost:5601", "defaultCommandTimeout": 120000, + "retries": { + "runMode": 2 + }, "screenshotsFolder": "../../../target/kibana-security-solution/cypress/screenshots", "trashAssetsBeforeRuns": false, "video": false, diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts index 7f02d41ad1b0c..db841d2a732c4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts @@ -158,6 +158,7 @@ describe('Alerts', () => { }); }); }); + context('Opening alerts', () => { beforeEach(() => { esArchiverLoad('closed_alerts'); @@ -204,6 +205,7 @@ describe('Alerts', () => { }); }); }); + context('Marking alerts as in-progress', () => { beforeEach(() => { esArchiverLoad('alerts'); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts index 20cf624b3360d..3fa304ab7cf19 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts @@ -43,6 +43,7 @@ describe('Alerts detection rules', () => { waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + cy.get(RULE_NAME) .eq(FIFTH_RULE) .invoke('text') @@ -56,7 +57,6 @@ describe('Alerts detection rules', () => { activateRule(SEVENTH_RULE); waitForRuleToBeActivated(); sortByActivatedRules(); - cy.get(RULE_NAME) .eq(FIRST_RULE) .invoke('text') @@ -70,7 +70,6 @@ describe('Alerts detection rules', () => { cy.wrap(expectedRulesNames).should('include', seventhRuleName); }); }); - cy.get(RULE_SWITCH).eq(FIRST_RULE).should('have.attr', 'role', 'switch'); cy.get(RULE_SWITCH).eq(SECOND_RULE).should('have.attr', 'role', 'switch'); }); 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 17ff1dad79960..f999c5cecc392 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,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { newRule, existingRule } from '../objects/rule'; +import { newRule, existingRule, indexPatterns, editedRule } from '../objects/rule'; +import { + ALERT_RULE_METHOD, + ALERT_RULE_NAME, + ALERT_RULE_RISK_SCORE, + ALERT_RULE_SEVERITY, + ALERT_RULE_VERSION, + NUMBER_OF_ALERTS, +} from '../screens/alerts'; import { CUSTOM_RULES_BTN, @@ -12,20 +20,49 @@ import { RULE_NAME, RULES_ROW, RULES_TABLE, + RULE_SWITCH, SEVERITY, SHOWING_RULES_TEXT, } from '../screens/alerts_detection_rules'; import { + ABOUT_CONTINUE_BTN, + ABOUT_EDIT_BUTTON, + ACTIONS_THROTTLE_INPUT, + CUSTOM_QUERY_INPUT, + DEFINE_CONTINUE_BUTTON, + DEFINE_EDIT_BUTTON, + DEFINE_INDEX_INPUT, + RISK_INPUT, + RULE_DESCRIPTION_INPUT, + RULE_NAME_INPUT, + SCHEDULE_INTERVAL_AMOUNT_INPUT, + SCHEDULE_INTERVAL_UNITS_INPUT, + SEVERITY_DROPDOWN, + TAGS_FIELD, +} from '../screens/create_new_rule'; +import { + ADDITIONAL_LOOK_BACK_DETAILS, + ABOUT_DETAILS, ABOUT_INVESTIGATION_NOTES, ABOUT_RULE_DESCRIPTION, + CUSTOM_QUERY_DETAILS, + DEFINITION_DETAILS, + FALSE_POSITIVES_DETAILS, + getDetails, + INDEX_PATTERNS_DETAILS, INVESTIGATION_NOTES_MARKDOWN, INVESTIGATION_NOTES_TOGGLE, + MITRE_ATTACK_DETAILS, + REFERENCE_URLS_DETAILS, + RISK_SCORE_DETAILS, RULE_ABOUT_DETAILS_HEADER_TOGGLE, RULE_NAME_HEADER, - getDescriptionForTitle, - ABOUT_DETAILS, - DEFINITION_DETAILS, + RULE_TYPE_DETAILS, + RUNS_EVERY_DETAILS, SCHEDULE_DETAILS, + SEVERITY_DETAILS, + TAGS_DETAILS, + TIMELINE_TEMPLATE_DETAILS, } from '../screens/rule_details'; import { @@ -37,46 +74,46 @@ import { changeToThreeHundredRowsPerPage, deleteFirstRule, deleteSelectedRules, + editFirstRule, filterByCustomRules, goToCreateNewRule, goToRuleDetails, selectNumberOfRules, waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, waitForRulesToBeLoaded, - editFirstRule, } from '../tasks/alerts_detection_rules'; import { createAndActivateRule, + fillAboutRule, fillAboutRuleAndContinue, fillDefineCustomRuleWithImportedQueryAndContinue, + fillScheduleRuleAndContinue, goToAboutStepTab, - goToScheduleStepTab, goToActionsStepTab, - fillAboutRule, + goToScheduleStepTab, + waitForTheRuleToBeExecuted, } from '../tasks/create_new_rule'; +import { saveEditedRule } from '../tasks/edit_rule'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; +import { refreshPage } from '../tasks/security_header'; 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', () => { +const expectedUrls = newRule.referenceUrls.join(''); +const expectedFalsePositives = newRule.falsePositivesExamples.join(''); +const expectedTags = newRule.tags.join(''); +const expectedMitre = newRule.mitre + .map(function (mitre) { + return mitre.tactic + mitre.techniques.join(''); + }) + .join(''); +const expectedNumberOfRules = 1; +const expectedEditedtags = editedRule.tags.join(''); +const expectedEditedIndexPatterns = + editedRule.index && editedRule.index.length ? editedRule.index : indexPatterns; + +describe('Custom detection rules creation', () => { before(() => { esArchiverLoad('timeline'); }); @@ -85,7 +122,7 @@ describe('Detection rules, custom', () => { esArchiverUnload('timeline'); }); - it('Creates and activates a new custom rule', () => { + it('Creates and activates a new rule', () => { loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); @@ -94,27 +131,27 @@ describe('Detection rules, custom', () => { goToCreateNewRule(); fillDefineCustomRuleWithImportedQueryAndContinue(newRule); fillAboutRuleAndContinue(newRule); + fillScheduleRuleAndContinue(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(CUSTOM_QUERY_INPUT).should('have.text', 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(RULE_NAME_INPUT).invoke('val').should('eql', 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)'); + cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); changeToThreeHundredRowsPerPage(); waitForRulesToBeLoaded(); - const expectedNumberOfRules = 1; cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); }); @@ -124,78 +161,59 @@ describe('Detection rules, custom', () => { cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', 1); }); - cy.get(RULE_NAME).invoke('text').should('eql', newRule.name); - cy.get(RISK_SCORE).invoke('text').should('eql', newRule.riskScore); - cy.get(SEVERITY).invoke('text').should('eql', newRule.severity); - cy.get('[data-test-subj="rule-switch"]').should('have.attr', 'aria-checked', 'true'); + cy.get(RULE_NAME).should('have.text', newRule.name); + cy.get(RISK_SCORE).should('have.text', newRule.riskScore); + cy.get(SEVERITY).should('have.text', newRule.severity); + cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); goToRuleDetails(); - let expectedUrls = ''; - newRule.referenceUrls.forEach((url) => { - expectedUrls = expectedUrls + url; - }); - let expectedFalsePositives = ''; - newRule.falsePositivesExamples.forEach((falsePositive) => { - expectedFalsePositives = expectedFalsePositives + falsePositive; - }); - let expectedTags = ''; - newRule.tags.forEach((tag) => { - expectedTags = expectedTags + tag; - }); - let expectedMitre = ''; - newRule.mitre.forEach((mitre) => { - expectedMitre = expectedMitre + mitre.tactic; - mitre.techniques.forEach((technique) => { - expectedMitre = expectedMitre + technique; - }); - }); - const expectedIndexPatterns = [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'winlogbeat-*', - ]; - - 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(RULE_NAME_HEADER).should('have.text', `${newRule.name} Beta`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newRule.description); 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); + getDetails(SEVERITY_DETAILS).should('have.text', newRule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', newRule.riskScore); + getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls); + getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); + getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre); + getDetails(TAGS_DETAILS).should('have.text', 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(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); 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'); + getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${newRule.customQuery} `); + getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query'); + getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); }); - cy.get(SCHEDULE_DETAILS).within(() => { - getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m'); - getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m'); + getDetails(RUNS_EVERY_DETAILS).should( + 'have.text', + `${newRule.runsEvery.interval}${newRule.runsEvery.type}` + ); + getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( + 'have.text', + `${newRule.lookBack.interval}${newRule.lookBack.type}` + ); }); + + refreshPage(); + waitForTheRuleToBeExecuted(); + + cy.get(NUMBER_OF_ALERTS) + .invoke('text') + .then((numberOfAlertsText) => { + cy.wrap(parseInt(numberOfAlertsText, 10)).should('be.above', 0); + }); + cy.get(ALERT_RULE_NAME).first().should('have.text', newRule.name); + cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); + cy.get(ALERT_RULE_METHOD).first().should('have.text', 'query'); + cy.get(ALERT_RULE_SEVERITY).first().should('have.text', newRule.severity.toLowerCase()); + cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', newRule.riskScore); }); }); -describe('Deletes custom rules', () => { +describe('Custom detection rules deletion and edition', () => { beforeEach(() => { esArchiverLoad('custom_rules'); loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); @@ -208,143 +226,132 @@ describe('Deletes custom rules', () => { esArchiverUnload('custom_rules'); }); - it('Deletes one rule', () => { - cy.get(RULES_TABLE) - .find(RULES_ROW) - .then((rules) => { - const initialNumberOfRules = rules.length; - const expectedNumberOfRulesAfterDeletion = initialNumberOfRules - 1; - - cy.get(SHOWING_RULES_TEXT) - .invoke('text') - .should('eql', `Showing ${initialNumberOfRules} rules`); - - deleteFirstRule(); - waitForRulesToBeLoaded(); - - cy.get(RULES_TABLE).then(($table) => { - cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterDeletion); - }); - cy.get(SHOWING_RULES_TEXT) - .invoke('text') - .should('eql', `Showing ${expectedNumberOfRulesAfterDeletion} rules`); - cy.get(CUSTOM_RULES_BTN) - .invoke('text') - .should('eql', `Custom rules (${expectedNumberOfRulesAfterDeletion})`); - }); - }); - - it('Deletes more than one rule', () => { - cy.get(RULES_TABLE) - .find(RULES_ROW) - .then((rules) => { - const initialNumberOfRules = rules.length; - const numberOfRulesToBeDeleted = 3; - const expectedNumberOfRulesAfterDeletion = initialNumberOfRules - numberOfRulesToBeDeleted; - - selectNumberOfRules(numberOfRulesToBeDeleted); - deleteSelectedRules(); - waitForRulesToBeLoaded(); - - cy.get(RULES_TABLE).then(($table) => { - cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterDeletion); + context('Deletion', () => { + it('Deletes one rule', () => { + cy.get(RULES_TABLE) + .find(RULES_ROW) + .then((rules) => { + const initialNumberOfRules = rules.length; + const expectedNumberOfRulesAfterDeletion = initialNumberOfRules - 1; + + cy.get(SHOWING_RULES_TEXT).should('have.text', `Showing ${initialNumberOfRules} rules`); + + deleteFirstRule(); + waitForRulesToBeLoaded(); + + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should( + 'eql', + expectedNumberOfRulesAfterDeletion + ); + }); + cy.get(SHOWING_RULES_TEXT).should( + 'have.text', + `Showing ${expectedNumberOfRulesAfterDeletion} rules` + ); + cy.get(CUSTOM_RULES_BTN).should( + 'have.text', + `Custom rules (${expectedNumberOfRulesAfterDeletion})` + ); }); - cy.get(SHOWING_RULES_TEXT) - .invoke('text') - .should('eql', `Showing ${expectedNumberOfRulesAfterDeletion} rule`); - cy.get(CUSTOM_RULES_BTN) - .invoke('text') - .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'); + it('Deletes more than one rule', () => { + cy.get(RULES_TABLE) + .find(RULES_ROW) + .then((rules) => { + const initialNumberOfRules = rules.length; + const numberOfRulesToBeDeleted = 3; + const expectedNumberOfRulesAfterDeletion = + initialNumberOfRules - numberOfRulesToBeDeleted; + + selectNumberOfRules(numberOfRulesToBeDeleted); + deleteSelectedRules(); + waitForRulesToBeLoaded(); + + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should( + 'eql', + expectedNumberOfRulesAfterDeletion + ); + }); + cy.get(SHOWING_RULES_TEXT).should( + 'have.text', + `Showing ${expectedNumberOfRulesAfterDeletion} rule` + ); + cy.get(CUSTOM_RULES_BTN).should( + 'have.text', + `Custom rules (${expectedNumberOfRulesAfterDeletion})` + ); + }); }); + }); - if (editedRule.interval) { - cy.get(SCHEDULE_DETAILS).within(() => { - getDescriptionForTitle('Runs every').invoke('text').should('eql', editedRule.interval); + context('Edition', () => { + it('Allows a rule to be edited', () => { + editFirstRule(); + + // expect define step to populate + cy.get(CUSTOM_QUERY_INPUT).should('have.text', existingRule.customQuery); + if (existingRule.index && existingRule.index.length > 0) { + cy.get(DEFINE_INDEX_INPUT).should('have.text', 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).should('have.text', existingRule.description); + cy.get(TAGS_FIELD).should('have.text', existingRule.tags.join('')); + cy.get(SEVERITY_DROPDOWN).should('have.text', 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(); + fillAboutRule(editedRule); + saveEditedRule(); + + cy.get(RULE_NAME_HEADER).should('have.text', `${editedRule.name} Beta`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', editedRule.description); + cy.get(ABOUT_DETAILS).within(() => { + getDetails(SEVERITY_DETAILS).should('have.text', editedRule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', editedRule.riskScore); + getDetails(TAGS_DETAILS).should('have.text', expectedEditedtags); + }); + cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE) + .eq(INVESTIGATION_NOTES_TOGGLE) + .click({ force: true }); + cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', editedRule.note); + cy.get(DEFINITION_DETAILS).within(() => { + getDetails(INDEX_PATTERNS_DETAILS).should( + 'have.text', + expectedEditedIndexPatterns.join('') + ); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${editedRule.customQuery} `); + getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query'); + getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); }); - } + if (editedRule.interval) { + cy.get(SCHEDULE_DETAILS).within(() => { + getDetails(RUNS_EVERY_DETAILS).should('have.text', 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 76871929fe050..e2ff51dd544a2 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { eqlRule } from '../objects/rule'; +import { eqlRule, indexPatterns } from '../objects/rule'; import { CUSTOM_RULES_BTN, @@ -12,19 +12,32 @@ import { RULE_NAME, RULES_ROW, RULES_TABLE, + RULE_SWITCH, SEVERITY, } from '../screens/alerts_detection_rules'; import { ABOUT_DETAILS, ABOUT_INVESTIGATION_NOTES, ABOUT_RULE_DESCRIPTION, + ADDITIONAL_LOOK_BACK_DETAILS, + CUSTOM_QUERY_DETAILS, DEFINITION_DETAILS, - getDescriptionForTitle, + FALSE_POSITIVES_DETAILS, + getDetails, + INDEX_PATTERNS_DETAILS, INVESTIGATION_NOTES_MARKDOWN, INVESTIGATION_NOTES_TOGGLE, + MITRE_ATTACK_DETAILS, + REFERENCE_URLS_DETAILS, + RISK_SCORE_DETAILS, RULE_ABOUT_DETAILS_HEADER_TOGGLE, RULE_NAME_HEADER, + RULE_TYPE_DETAILS, + RUNS_EVERY_DETAILS, SCHEDULE_DETAILS, + SEVERITY_DETAILS, + TAGS_DETAILS, + TIMELINE_TEMPLATE_DETAILS, } from '../screens/rule_details'; import { @@ -43,14 +56,25 @@ import { import { createAndActivateRule, fillAboutRuleAndContinue, - selectEqlRuleType, fillDefineEqlRuleAndContinue, + fillScheduleRuleAndContinue, + selectEqlRuleType, } from '../tasks/create_new_rule'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; +const expectedUrls = eqlRule.referenceUrls.join(''); +const expectedFalsePositives = eqlRule.falsePositivesExamples.join(''); +const expectedTags = eqlRule.tags.join(''); +const expectedMitre = eqlRule.mitre + .map(function (mitre) { + return mitre.tactic + mitre.techniques.join(''); + }) + .join(''); +const expectedNumberOfRules = 1; + describe('Detection rules, EQL', () => { before(() => { esArchiverLoad('timeline'); @@ -70,14 +94,14 @@ describe('Detection rules, EQL', () => { selectEqlRuleType(); fillDefineEqlRuleAndContinue(eqlRule); fillAboutRuleAndContinue(eqlRule); + fillScheduleRuleAndContinue(eqlRule); createAndActivateRule(); - cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)'); + cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); changeToThreeHundredRowsPerPage(); waitForRulesToBeLoaded(); - const expectedNumberOfRules = 1; cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); }); @@ -87,73 +111,40 @@ describe('Detection rules, EQL', () => { cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', 1); }); - cy.get(RULE_NAME).invoke('text').should('eql', eqlRule.name); - cy.get(RISK_SCORE).invoke('text').should('eql', eqlRule.riskScore); - cy.get(SEVERITY).invoke('text').should('eql', eqlRule.severity); - cy.get('[data-test-subj="rule-switch"]').should('have.attr', 'aria-checked', 'true'); + cy.get(RULE_NAME).should('have.text', eqlRule.name); + cy.get(RISK_SCORE).should('have.text', eqlRule.riskScore); + cy.get(SEVERITY).should('have.text', eqlRule.severity); + cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); goToRuleDetails(); - let expectedUrls = ''; - eqlRule.referenceUrls.forEach((url) => { - expectedUrls = expectedUrls + url; - }); - let expectedFalsePositives = ''; - eqlRule.falsePositivesExamples.forEach((falsePositive) => { - expectedFalsePositives = expectedFalsePositives + falsePositive; - }); - let expectedTags = ''; - eqlRule.tags.forEach((tag) => { - expectedTags = expectedTags + tag; - }); - let expectedMitre = ''; - eqlRule.mitre.forEach((mitre) => { - expectedMitre = expectedMitre + mitre.tactic; - mitre.techniques.forEach((technique) => { - expectedMitre = expectedMitre + technique; - }); - }); - const expectedIndexPatterns = [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'winlogbeat-*', - ]; - - 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(RULE_NAME_HEADER).should('have.text', `${eqlRule.name} Beta`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', eqlRule.description); 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); + getDetails(SEVERITY_DETAILS).should('have.text', eqlRule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', eqlRule.riskScore); + getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls); + getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); + getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre); + getDetails(TAGS_DETAILS).should('have.text', 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(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); 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'); + getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${eqlRule.customQuery} `); + getDetails(RULE_TYPE_DETAILS).should('have.text', 'Event Correlation'); + getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); }); - cy.get(SCHEDULE_DETAILS).within(() => { - getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m'); - getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m'); + getDetails(RUNS_EVERY_DETAILS).should( + 'have.text', + `${eqlRule.runsEvery.interval}${eqlRule.runsEvery.type}` + ); + getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( + 'have.text', + `${eqlRule.lookBack.interval}${eqlRule.lookBack.type}` + ); }); }); }); 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 47e49d48e2aec..49ec6381cbc89 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,14 +16,25 @@ import { SEVERITY, } from '../screens/alerts_detection_rules'; import { + ABOUT_DETAILS, ABOUT_RULE_DESCRIPTION, + ADDITIONAL_LOOK_BACK_DETAILS, + ANOMALY_SCORE_DETAILS, + DEFINITION_DETAILS, + FALSE_POSITIVES_DETAILS, + getDetails, MACHINE_LEARNING_JOB_ID, MACHINE_LEARNING_JOB_STATUS, + MITRE_ATTACK_DETAILS, + REFERENCE_URLS_DETAILS, + RISK_SCORE_DETAILS, RULE_NAME_HEADER, - getDescriptionForTitle, - ABOUT_DETAILS, - DEFINITION_DETAILS, + RULE_TYPE_DETAILS, + RUNS_EVERY_DETAILS, SCHEDULE_DETAILS, + SEVERITY_DETAILS, + TAGS_DETAILS, + TIMELINE_TEMPLATE_DETAILS, } from '../screens/rule_details'; import { @@ -43,6 +54,7 @@ import { createAndActivateRule, fillAboutRuleAndContinue, fillDefineMachineLearningRuleAndContinue, + fillScheduleRuleAndContinue, selectMachineLearningRuleType, } from '../tasks/create_new_rule'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; @@ -50,6 +62,16 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; +const expectedUrls = machineLearningRule.referenceUrls.join(''); +const expectedFalsePositives = machineLearningRule.falsePositivesExamples.join(''); +const expectedTags = machineLearningRule.tags.join(''); +const expectedMitre = machineLearningRule.mitre + .map(function (mitre) { + return mitre.tactic + mitre.techniques.join(''); + }) + .join(''); +const expectedNumberOfRules = totalNumberOfPrebuiltRulesInEsArchive + 1; + describe('Detection rules, machine learning', () => { before(() => { esArchiverLoad('prebuilt_rules_loaded'); @@ -69,6 +91,7 @@ describe('Detection rules, machine learning', () => { selectMachineLearningRuleType(); fillDefineMachineLearningRuleAndContinue(machineLearningRule); fillAboutRuleAndContinue(machineLearningRule); + fillScheduleRuleAndContinue(machineLearningRule); createAndActivateRule(); cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)'); @@ -76,7 +99,6 @@ describe('Detection rules, machine learning', () => { changeToThreeHundredRowsPerPage(); waitForRulesToBeLoaded(); - const expectedNumberOfRules = totalNumberOfPrebuiltRulesInEsArchive + 1; cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); }); @@ -86,67 +108,42 @@ describe('Detection rules, machine learning', () => { cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', 1); }); - cy.get(RULE_NAME).invoke('text').should('eql', machineLearningRule.name); - cy.get(RISK_SCORE).invoke('text').should('eql', machineLearningRule.riskScore); - cy.get(SEVERITY).invoke('text').should('eql', machineLearningRule.severity); + cy.get(RULE_NAME).should('have.text', machineLearningRule.name); + cy.get(RISK_SCORE).should('have.text', machineLearningRule.riskScore); + cy.get(SEVERITY).should('have.text', machineLearningRule.severity); cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); goToRuleDetails(); - let expectedUrls = ''; - machineLearningRule.referenceUrls.forEach((url) => { - expectedUrls = expectedUrls + url; - }); - let expectedFalsePositives = ''; - machineLearningRule.falsePositivesExamples.forEach((falsePositive) => { - expectedFalsePositives = expectedFalsePositives + falsePositive; - }); - let expectedTags = ''; - machineLearningRule.tags.forEach((tag) => { - expectedTags = expectedTags + tag; - }); - let expectedMitre = ''; - machineLearningRule.mitre.forEach((mitre) => { - expectedMitre = expectedMitre + mitre.tactic; - mitre.techniques.forEach((technique) => { - expectedMitre = expectedMitre + technique; - }); - }); - - 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(RULE_NAME_HEADER).should('have.text', `${machineLearningRule.name} Beta`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', machineLearningRule.description); 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); + getDetails(SEVERITY_DETAILS).should('have.text', machineLearningRule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', machineLearningRule.riskScore); + getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls); + getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); + getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre); + getDetails(TAGS_DETAILS).should('have.text', 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); + getDetails(ANOMALY_SCORE_DETAILS).should( + 'have.text', + machineLearningRule.anomalyScoreThreshold + ); + getDetails(RULE_TYPE_DETAILS).should('have.text', 'Machine Learning'); + getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); + cy.get(MACHINE_LEARNING_JOB_STATUS).should('have.text', 'Stopped'); + cy.get(MACHINE_LEARNING_JOB_ID).should('have.text', 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'); + getDetails(RUNS_EVERY_DETAILS).should( + 'have.text', + `${machineLearningRule.runsEvery.interval}${machineLearningRule.runsEvery.type}` + ); + getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( + 'have.text', + `${machineLearningRule.lookBack.interval}${machineLearningRule.lookBack.type}` + ); }); }); }); 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 4edf5e1866087..090012de72534 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 @@ -4,33 +4,58 @@ * you may not use this file except in compliance with the Elastic License. */ -import { newOverrideRule } from '../objects/rule'; +import { indexPatterns, newOverrideRule, severitiesOverride } from '../objects/rule'; +import { + NUMBER_OF_ALERTS, + ALERT_RULE_NAME, + ALERT_RULE_METHOD, + ALERT_RULE_RISK_SCORE, + ALERT_RULE_SEVERITY, + ALERT_RULE_VERSION, +} from '../screens/alerts'; import { CUSTOM_RULES_BTN, RISK_SCORE, RULE_NAME, + RULE_SWITCH, RULES_ROW, RULES_TABLE, SEVERITY, } from '../screens/alerts_detection_rules'; import { ABOUT_INVESTIGATION_NOTES, + ABOUT_DETAILS, ABOUT_RULE_DESCRIPTION, + ADDITIONAL_LOOK_BACK_DETAILS, + CUSTOM_QUERY_DETAILS, + DEFINITION_DETAILS, + DETAILS_DESCRIPTION, + DETAILS_TITLE, + FALSE_POSITIVES_DETAILS, + getDetails, + INDEX_PATTERNS_DETAILS, INVESTIGATION_NOTES_MARKDOWN, INVESTIGATION_NOTES_TOGGLE, + MITRE_ATTACK_DETAILS, + REFERENCE_URLS_DETAILS, + RISK_SCORE_DETAILS, + RISK_SCORE_OVERRIDE_DETAILS, RULE_ABOUT_DETAILS_HEADER_TOGGLE, RULE_NAME_HEADER, - ABOUT_DETAILS, - getDescriptionForTitle, - DEFINITION_DETAILS, + RULE_NAME_OVERRIDE_DETAILS, + RULE_TYPE_DETAILS, + RUNS_EVERY_DETAILS, SCHEDULE_DETAILS, - DETAILS_TITLE, - DETAILS_DESCRIPTION, + SEVERITY_DETAILS, + TAGS_DETAILS, + TIMELINE_TEMPLATE_DETAILS, + TIMESTAMP_OVERRIDE_DETAILS, } from '../screens/rule_details'; import { goToManageAlertsDetectionRules, + sortRiskScore, waitForAlertsIndexToBeCreated, waitForAlertsPanelToBeLoaded, } from '../tasks/alerts'; @@ -46,12 +71,24 @@ import { createAndActivateRule, fillAboutRuleWithOverrideAndContinue, fillDefineCustomRuleWithImportedQueryAndContinue, + fillScheduleRuleAndContinue, + waitForTheRuleToBeExecuted, } from '../tasks/create_new_rule'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; +import { refreshPage } from '../tasks/security_header'; import { DETECTIONS_URL } from '../urls/navigation'; +const expectedUrls = newOverrideRule.referenceUrls.join(''); +const expectedFalsePositives = newOverrideRule.falsePositivesExamples.join(''); +const expectedTags = newOverrideRule.tags.join(''); +const expectedMitre = newOverrideRule.mitre + .map(function (mitre) { + return mitre.tactic + mitre.techniques.join(''); + }) + .join(''); + describe('Detection rules, override', () => { before(() => { esArchiverLoad('timeline'); @@ -70,9 +107,10 @@ describe('Detection rules, override', () => { goToCreateNewRule(); fillDefineCustomRuleWithImportedQueryAndContinue(newOverrideRule); fillAboutRuleWithOverrideAndContinue(newOverrideRule); + fillScheduleRuleAndContinue(newOverrideRule); createAndActivateRule(); - cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)'); + cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); changeToThreeHundredRowsPerPage(); waitForRulesToBeLoaded(); @@ -87,98 +125,75 @@ describe('Detection rules, override', () => { cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', 1); }); - cy.get(RULE_NAME).invoke('text').should('eql', newOverrideRule.name); - cy.get(RISK_SCORE).invoke('text').should('eql', newOverrideRule.riskScore); - cy.get(SEVERITY).invoke('text').should('eql', newOverrideRule.severity); - cy.get('[data-test-subj="rule-switch"]').should('have.attr', 'aria-checked', 'true'); + cy.get(RULE_NAME).should('have.text', newOverrideRule.name); + cy.get(RISK_SCORE).should('have.text', newOverrideRule.riskScore); + cy.get(SEVERITY).should('have.text', newOverrideRule.severity); + cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); goToRuleDetails(); - let expectedUrls = ''; - newOverrideRule.referenceUrls.forEach((url) => { - expectedUrls = expectedUrls + url; - }); - let expectedFalsePositives = ''; - newOverrideRule.falsePositivesExamples.forEach((falsePositive) => { - expectedFalsePositives = expectedFalsePositives + falsePositive; - }); - let expectedTags = ''; - newOverrideRule.tags.forEach((tag) => { - expectedTags = expectedTags + tag; - }); - let expectedMitre = ''; - newOverrideRule.mitre.forEach((mitre) => { - expectedMitre = expectedMitre + mitre.tactic; - mitre.techniques.forEach((technique) => { - expectedMitre = expectedMitre + technique; - }); - }); - const expectedIndexPatterns = [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'winlogbeat-*', - ]; - - cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${newOverrideRule.name} Beta`); - - cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', newOverrideRule.description); - - const expectedOverrideSeverities = ['Low', 'Medium', 'High', 'Critical']; - + cy.get(RULE_NAME_HEADER).should('have.text', `${newOverrideRule.name} Beta`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newOverrideRule.description); 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', `${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); + getDetails(SEVERITY_DETAILS).should('have.text', newOverrideRule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', newOverrideRule.riskScore); + getDetails(RISK_SCORE_OVERRIDE_DETAILS).should( + 'have.text', + `${newOverrideRule.riskOverride}signal.rule.risk_score` + ); + getDetails(RULE_NAME_OVERRIDE_DETAILS).should('have.text', newOverrideRule.nameOverride); + getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls); + getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); + getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre); + getDetails(TAGS_DETAILS).should('have.text', expectedTags); + getDetails(TIMESTAMP_OVERRIDE_DETAILS).should('have.text', 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]}` + 'have.text', + `${severity.sourceField}:${severity.sourceValue}${severitiesOverride[i]}` ); }); }); }); - 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(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); 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'); + getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${newOverrideRule.customQuery} `); + getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query'); + getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); }); - cy.get(SCHEDULE_DETAILS).within(() => { - getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m'); - getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m'); + getDetails(RUNS_EVERY_DETAILS).should( + 'have.text', + `${newOverrideRule.runsEvery.interval}${newOverrideRule.runsEvery.type}` + ); + getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( + 'have.text', + `${newOverrideRule.lookBack.interval}${newOverrideRule.lookBack.type}` + ); }); + + refreshPage(); + waitForTheRuleToBeExecuted(); + + cy.get(NUMBER_OF_ALERTS) + .invoke('text') + .then((numberOfAlertsText) => { + cy.wrap(parseInt(numberOfAlertsText, 10)).should('be.above', 0); + }); + cy.get(ALERT_RULE_NAME).first().should('have.text', 'auditbeat'); + cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); + cy.get(ALERT_RULE_METHOD).first().should('have.text', 'query'); + cy.get(ALERT_RULE_SEVERITY).first().should('have.text', 'critical'); + + sortRiskScore(); + + cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', '80'); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts index 986a7c7177a79..6088a9dedbd06 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts @@ -56,7 +56,7 @@ describe('Alerts rules, prebuilt rules', () => { loadPrebuiltDetectionRules(); waitForPrebuiltDetectionRulesToBeLoaded(); - cy.get(ELASTIC_RULES_BTN).invoke('text').should('eql', expectedElasticRulesBtnText); + cy.get(ELASTIC_RULES_BTN).should('have.text', expectedElasticRulesBtnText); changeToThreeHundredRowsPerPage(); waitForRulesToBeLoaded(); @@ -81,7 +81,7 @@ describe('Deleting prebuilt rules', () => { loadPrebuiltDetectionRules(); waitForPrebuiltDetectionRulesToBeLoaded(); - cy.get(ELASTIC_RULES_BTN).invoke('text').should('eql', expectedElasticRulesBtnText); + cy.get(ELASTIC_RULES_BTN).should('have.text', expectedElasticRulesBtnText); changeToThreeHundredRowsPerPage(); waitForRulesToBeLoaded(); @@ -113,16 +113,15 @@ describe('Deleting prebuilt rules', () => { changeToThreeHundredRowsPerPage(); waitForRulesToBeLoaded(); - cy.get(ELASTIC_RULES_BTN) - .invoke('text') - .should('eql', `Elastic rules (${expectedNumberOfRulesAfterDeletion})`); + cy.get(ELASTIC_RULES_BTN).should( + 'have.text', + `Elastic rules (${expectedNumberOfRulesAfterDeletion})` + ); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterDeletion); }); cy.get(RELOAD_PREBUILT_RULES_BTN).should('exist'); - cy.get(RELOAD_PREBUILT_RULES_BTN) - .invoke('text') - .should('eql', 'Install 1 Elastic prebuilt rule '); + cy.get(RELOAD_PREBUILT_RULES_BTN).should('have.text', 'Install 1 Elastic prebuilt rule '); reloadDeletedRules(); @@ -135,9 +134,10 @@ describe('Deleting prebuilt rules', () => { cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterRecovering); }); - cy.get(ELASTIC_RULES_BTN) - .invoke('text') - .should('eql', `Elastic rules (${expectedNumberOfRulesAfterRecovering})`); + cy.get(ELASTIC_RULES_BTN).should( + 'have.text', + `Elastic rules (${expectedNumberOfRulesAfterRecovering})` + ); }); it('Deletes and recovers more than one rule', () => { @@ -152,12 +152,14 @@ describe('Deleting prebuilt rules', () => { waitForRulesToBeLoaded(); cy.get(RELOAD_PREBUILT_RULES_BTN).should('exist'); - cy.get(RELOAD_PREBUILT_RULES_BTN) - .invoke('text') - .should('eql', `Install ${numberOfRulesToBeSelected} Elastic prebuilt rules `); - cy.get(ELASTIC_RULES_BTN) - .invoke('text') - .should('eql', `Elastic rules (${expectedNumberOfRulesAfterDeletion})`); + cy.get(RELOAD_PREBUILT_RULES_BTN).should( + 'have.text', + `Install ${numberOfRulesToBeSelected} Elastic prebuilt rules ` + ); + cy.get(ELASTIC_RULES_BTN).should( + 'have.text', + `Elastic rules (${expectedNumberOfRulesAfterDeletion})` + ); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterDeletion); }); @@ -173,8 +175,9 @@ describe('Deleting prebuilt rules', () => { cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterRecovering); }); - cy.get(ELASTIC_RULES_BTN) - .invoke('text') - .should('eql', `Elastic rules (${expectedNumberOfRulesAfterRecovering})`); + cy.get(ELASTIC_RULES_BTN).should( + 'have.text', + `Elastic rules (${expectedNumberOfRulesAfterRecovering})` + ); }); }); 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 00175ed3baeb8..5ee7e69e877e3 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 @@ -4,27 +4,49 @@ * you may not use this file except in compliance with the Elastic License. */ -import { newThresholdRule } from '../objects/rule'; +import { indexPatterns, newThresholdRule } from '../objects/rule'; +import { + ALERT_RULE_METHOD, + ALERT_RULE_NAME, + ALERT_RULE_RISK_SCORE, + ALERT_RULE_SEVERITY, + ALERT_RULE_VERSION, + NUMBER_OF_ALERTS, +} from '../screens/alerts'; import { CUSTOM_RULES_BTN, RISK_SCORE, RULE_NAME, + RULE_SWITCH, RULES_ROW, RULES_TABLE, SEVERITY, } from '../screens/alerts_detection_rules'; import { + ABOUT_DETAILS, ABOUT_INVESTIGATION_NOTES, ABOUT_RULE_DESCRIPTION, + ADDITIONAL_LOOK_BACK_DETAILS, + CUSTOM_QUERY_DETAILS, + FALSE_POSITIVES_DETAILS, + DEFINITION_DETAILS, + getDetails, + INDEX_PATTERNS_DETAILS, INVESTIGATION_NOTES_MARKDOWN, INVESTIGATION_NOTES_TOGGLE, + MITRE_ATTACK_DETAILS, + REFERENCE_URLS_DETAILS, + RISK_SCORE_DETAILS, RULE_ABOUT_DETAILS_HEADER_TOGGLE, RULE_NAME_HEADER, - getDescriptionForTitle, - ABOUT_DETAILS, - DEFINITION_DETAILS, + RULE_TYPE_DETAILS, + RUNS_EVERY_DETAILS, SCHEDULE_DETAILS, + SEVERITY_DETAILS, + TAGS_DETAILS, + THRESHOLD_DETAILS, + TIMELINE_TEMPLATE_DETAILS, } from '../screens/rule_details'; import { @@ -44,13 +66,25 @@ import { createAndActivateRule, fillAboutRuleAndContinue, fillDefineThresholdRuleAndContinue, + fillScheduleRuleAndContinue, selectThresholdRuleType, + waitForTheRuleToBeExecuted, } from '../tasks/create_new_rule'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; +import { refreshPage } from '../tasks/security_header'; import { DETECTIONS_URL } from '../urls/navigation'; +const expectedUrls = newThresholdRule.referenceUrls.join(''); +const expectedFalsePositives = newThresholdRule.falsePositivesExamples.join(''); +const expectedTags = newThresholdRule.tags.join(''); +const expectedMitre = newThresholdRule.mitre + .map(function (mitre) { + return mitre.tactic + mitre.techniques.join(''); + }) + .join(''); + describe('Detection rules, threshold', () => { before(() => { esArchiverLoad('timeline'); @@ -70,9 +104,10 @@ describe('Detection rules, threshold', () => { selectThresholdRuleType(); fillDefineThresholdRuleAndContinue(newThresholdRule); fillAboutRuleAndContinue(newThresholdRule); + fillScheduleRuleAndContinue(newThresholdRule); createAndActivateRule(); - cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)'); + cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); changeToThreeHundredRowsPerPage(); waitForRulesToBeLoaded(); @@ -87,79 +122,60 @@ describe('Detection rules, threshold', () => { cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', 1); }); - cy.get(RULE_NAME).invoke('text').should('eql', newThresholdRule.name); - cy.get(RISK_SCORE).invoke('text').should('eql', newThresholdRule.riskScore); - cy.get(SEVERITY).invoke('text').should('eql', newThresholdRule.severity); - cy.get('[data-test-subj="rule-switch"]').should('have.attr', 'aria-checked', 'true'); + cy.get(RULE_NAME).should('have.text', newThresholdRule.name); + cy.get(RISK_SCORE).should('have.text', newThresholdRule.riskScore); + cy.get(SEVERITY).should('have.text', newThresholdRule.severity); + cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); goToRuleDetails(); - let expectedUrls = ''; - newThresholdRule.referenceUrls.forEach((url) => { - expectedUrls = expectedUrls + url; - }); - let expectedFalsePositives = ''; - newThresholdRule.falsePositivesExamples.forEach((falsePositive) => { - expectedFalsePositives = expectedFalsePositives + falsePositive; - }); - let expectedTags = ''; - newThresholdRule.tags.forEach((tag) => { - expectedTags = expectedTags + tag; - }); - let expectedMitre = ''; - newThresholdRule.mitre.forEach((mitre) => { - expectedMitre = expectedMitre + mitre.tactic; - mitre.techniques.forEach((technique) => { - expectedMitre = expectedMitre + technique; - }); - }); - const expectedIndexPatterns = [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'winlogbeat-*', - ]; - - 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(RULE_NAME_HEADER).should('have.text', `${newThresholdRule.name} Beta`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newThresholdRule.description); 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); + getDetails(SEVERITY_DETAILS).should('have.text', newThresholdRule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', newThresholdRule.riskScore); + getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls); + getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); + getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre); + getDetails(TAGS_DETAILS).should('have.text', 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(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); 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}` - ); + getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${newThresholdRule.customQuery} `); + getDetails(RULE_TYPE_DETAILS).should('have.text', 'Threshold'); + getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); + getDetails(THRESHOLD_DETAILS).should( + 'have.text', + `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'); + getDetails(RUNS_EVERY_DETAILS).should( + 'have.text', + `${newThresholdRule.runsEvery.interval}${newThresholdRule.runsEvery.type}` + ); + getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( + 'have.text', + `${newThresholdRule.lookBack.interval}${newThresholdRule.lookBack.type}` + ); }); + + refreshPage(); + waitForTheRuleToBeExecuted(); + + cy.get(NUMBER_OF_ALERTS) + .invoke('text') + .then((numberOfAlertsText) => { + cy.wrap(parseInt(numberOfAlertsText, 10)).should('be.below', 100); + }); + cy.get(ALERT_RULE_NAME).first().should('have.text', newThresholdRule.name); + cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); + cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threshold'); + cy.get(ALERT_RULE_SEVERITY) + .first() + .should('have.text', newThresholdRule.severity.toLowerCase()); + cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', newThresholdRule.riskScore); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts index 2fed23755963b..31d8e4666d91d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts @@ -35,7 +35,7 @@ describe('Alerts timeline', () => { .invoke('text') .then((eventId) => { investigateFirstAlertInTimeline(); - cy.get(PROVIDER_BADGE).invoke('text').should('eql', `_id: "${eventId}"`); + cy.get(PROVIDER_BADGE).should('have.text', `_id: "${eventId}"`); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts index 2588c580dedd3..906fba28a7721 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts @@ -14,7 +14,7 @@ import { import { HOSTS_NAMES } from '../screens/hosts/all_hosts'; import { ANOMALIES_TAB } from '../screens/hosts/main'; import { BREADCRUMBS, HOSTS, KQL_INPUT, NETWORK } from '../screens/security_header'; -import { SERVER_SIDE_EVENT_COUNT, TIMELINE_TITLE } from '../screens/timeline'; +import { TIMELINE_TITLE } from '../screens/timeline'; import { loginAndWaitForPage, loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { @@ -32,16 +32,18 @@ import { waitForIpsTableToBeLoaded } from '../tasks/network/flows'; import { clearSearchBar, kqlSearch, navigateFromHeaderTo } from '../tasks/security_header'; import { openTimelineUsingToggle } from '../tasks/security_main'; import { - addDescriptionToTimeline, addNameToTimeline, closeTimeline, - executeTimelineKQL, + populateTimeline, waitForTimelineChanges, } from '../tasks/timeline'; import { HOSTS_URL } from '../urls/navigation'; import { ABSOLUTE_DATE_RANGE } from '../urls/state'; +import { timeline } from '../objects/timeline'; +import { TIMELINE } from '../screens/create_new_case'; + const ABSOLUTE_DATE = { endTime: '2019-08-01T20:33:29.186Z', endTimeTimeline: '2019-08-02T21:03:29.186Z', @@ -51,8 +53,7 @@ const ABSOLUTE_DATE = { startTimeTimeline: '2019-08-02T20:03:29.186Z', }; -// FLAKY: https://github.com/elastic/kibana/issues/61612 -describe.skip('url state', () => { +describe('url state', () => { it('sets the global start and end dates from the url', () => { loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.url); cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON).should( @@ -222,23 +223,12 @@ describe.skip('url state', () => { it('sets and reads the url state for timeline by id', () => { loginAndWaitForPage(HOSTS_URL); openTimelineUsingToggle(); - executeTimelineKQL('host.name: *'); - - cy.get(SERVER_SIDE_EVENT_COUNT) - .invoke('text') - .then((strCount) => { - const intCount = +strCount; - cy.wrap(intCount).should('be.above', 0); - }); + populateTimeline(); cy.server(); cy.route('PATCH', '**/api/timeline').as('timeline'); - const timelineName = 'Security'; - const timelineDescription = 'This is the best timeline of the world'; - addNameToTimeline(timelineName); - waitForTimelineChanges(); - addDescriptionToTimeline(timelineDescription); + addNameToTimeline(timeline.title); waitForTimelineChanges(); cy.wait('@timeline').then((response) => { @@ -249,9 +239,10 @@ describe.skip('url state', () => { cy.visit('/app/home'); cy.visit(`/app/security/timelines?timeline=(id:'${timelineId}',isOpen:!t)`); cy.get(DATE_PICKER_APPLY_BUTTON_TIMELINE).should('exist'); - cy.get(DATE_PICKER_APPLY_BUTTON_TIMELINE).invoke('text').should('not.equal', 'Updating'); + cy.get(DATE_PICKER_APPLY_BUTTON_TIMELINE).should('not.have.text', 'Updating'); + cy.get(TIMELINE).should('be.visible'); cy.get(TIMELINE_TITLE).should('be.visible'); - cy.get(TIMELINE_TITLE).should('have.attr', 'value', timelineName); + cy.get(TIMELINE_TITLE).should('have.attr', 'value', timeline.title); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 2a5c60815f450..e84e2b7b1669f 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -23,6 +23,12 @@ interface SeverityOverride { sourceValue: string; } +interface Interval { + interval: string; + timeType: string; + type: string; +} + export interface CustomRule { customQuery: string; name: string; @@ -38,6 +44,8 @@ export interface CustomRule { mitre: Mitre[]; note: string; timelineId: string; + runsEvery: Interval; + lookBack: Interval; } export interface ThresholdRule extends CustomRule { @@ -65,6 +73,8 @@ export interface MachineLearningRule { falsePositivesExamples: string[]; mitre: Mitre[]; note: string; + runsEvery: Interval; + lookBack: Interval; } const mitre1: Mitre = { @@ -83,8 +93,8 @@ const severityOverride1: SeverityOverride = { }; const severityOverride2: SeverityOverride = { - sourceField: 'agent.type', - sourceValue: 'endpoint', + sourceField: '@timestamp', + sourceValue: '10/02/2020', }; const severityOverride3: SeverityOverride = { @@ -93,8 +103,20 @@ const severityOverride3: SeverityOverride = { }; const severityOverride4: SeverityOverride = { - sourceField: '@timestamp', - sourceValue: '10/02/2020', + sourceField: 'agent.type', + sourceValue: 'auditbeat', +}; + +const runsEvery: Interval = { + interval: '1', + timeType: 'Seconds', + type: 's', +}; + +const lookBack: Interval = { + interval: '17520', + timeType: 'Hours', + type: 'h', }; export const newRule: CustomRule = { @@ -109,6 +131,8 @@ export const newRule: CustomRule = { mitre: [mitre1, mitre2], note: '# test markdown', timelineId: '0162c130-78be-11ea-9718-118a926974a4', + runsEvery, + lookBack, }; export const existingRule: CustomRule = { @@ -132,6 +156,8 @@ export const existingRule: CustomRule = { mitre: [], note: 'This is my note', timelineId: '', + runsEvery, + lookBack, }; export const newOverrideRule: OverrideRule = { @@ -150,6 +176,8 @@ export const newOverrideRule: OverrideRule = { riskOverride: 'destination.port', nameOverride: 'agent.type', timestampOverride: '@timestamp', + runsEvery, + lookBack, }; export const newThresholdRule: ThresholdRule = { @@ -166,6 +194,8 @@ export const newThresholdRule: ThresholdRule = { timelineId: '0162c130-78be-11ea-9718-118a926974a4', thresholdField: 'host.name', threshold: '10', + runsEvery, + lookBack, }; export const machineLearningRule: MachineLearningRule = { @@ -180,6 +210,8 @@ export const machineLearningRule: MachineLearningRule = { falsePositivesExamples: ['False1'], mitre: [mitre1], note: '# test markdown', + runsEvery, + lookBack, }; export const eqlRule: CustomRule = { @@ -194,4 +226,24 @@ export const eqlRule: CustomRule = { mitre: [mitre1, mitre2], note: '# test markdown', timelineId: '0162c130-78be-11ea-9718-118a926974a4', + runsEvery, + lookBack, +}; + +export const indexPatterns = [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', +]; + +export const severitiesOverride = ['Low', 'Medium', 'High', 'Critical']; + +export const editedRule = { + ...existingRule, + severity: 'Medium', + description: 'Edited Rule description', }; diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts.ts b/x-pack/plugins/security_solution/cypress/screens/alerts.ts index fb7e7e73986b9..ed05874bd4c4d 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts.ts @@ -4,45 +4,57 @@ * you may not use this file except in compliance with the Elastic License. */ -export const EXPAND_ALERT_BTN = '[data-test-subj="expand-event"]'; +export const ALERTS = '[data-test-subj="event"]'; -export const LOADING_ALERTS_PANEL = '[data-test-subj="loading-alerts-panel"]'; +export const ALERT_CHECKBOX = '[data-test-subj="select-event-container"] .euiCheckbox__input'; -export const MANAGE_ALERT_DETECTION_RULES_BTN = '[data-test-subj="manage-alert-detection-rules"]'; +export const ALERT_ID = '[data-test-subj="draggable-content-_id"]'; -export const NUMBER_OF_ALERTS = '[data-test-subj="server-side-event-count"] .euiBadge__text'; +export const ALERT_RISK_SCORE_HEADER = '[data-test-subj="header-text-signal.rule.risk_score"]'; -export const OPENED_ALERTS_FILTER_BTN = '[data-test-subj="openAlerts"]'; +export const ALERT_RULE_METHOD = '[data-test-subj="draggable-content-signal.rule.type"]'; -export const CLOSED_ALERTS_FILTER_BTN = '[data-test-subj="closedAlerts"]'; +export const ALERT_RULE_NAME = '[data-test-subj="draggable-content-signal.rule.name"]'; -export const IN_PROGRESS_ALERTS_FILTER_BTN = '[data-test-subj="inProgressAlerts"]'; +export const ALERT_RULE_RISK_SCORE = '[data-test-subj="draggable-content-signal.rule.risk_score"]'; -export const SELECTED_ALERTS = '[data-test-subj="selectedAlerts"]'; +export const ALERT_RULE_SEVERITY = '[data-test-subj="draggable-content-signal.rule.severity"]'; -export const SEND_ALERT_TO_TIMELINE_BTN = '[data-test-subj="send-alert-to-timeline-button"]'; +export const ALERT_RULE_VERSION = '[data-test-subj="draggable-content-signal.rule.version"]'; -export const SHOWING_ALERTS = '[data-test-subj="showingAlerts"]'; +export const CLOSE_ALERT_BTN = '[data-test-subj="close-alert-status"]'; -export const ALERTS = '[data-test-subj="event"]'; +export const CLOSE_SELECTED_ALERTS_BTN = '[data-test-subj="closeSelectedAlertsButton"]'; -export const ALERT_ID = '[data-test-subj="draggable-content-_id"]'; +export const CLOSED_ALERTS_FILTER_BTN = '[data-test-subj="closedAlerts"]'; -export const ALERT_CHECKBOX = '[data-test-subj="select-event-container"] .euiCheckbox__input'; +export const EXPAND_ALERT_BTN = '[data-test-subj="expand-event"]'; -export const TAKE_ACTION_POPOVER_BTN = '[data-test-subj="alertActionPopover"] button'; +export const IN_PROGRESS_ALERTS_FILTER_BTN = '[data-test-subj="inProgressAlerts"]'; -export const TIMELINE_CONTEXT_MENU_BTN = '[data-test-subj="timeline-context-menu-button"]'; +export const LOADING_ALERTS_PANEL = '[data-test-subj="loading-alerts-panel"]'; -export const OPEN_SELECTED_ALERTS_BTN = '[data-test-subj="openSelectedAlertsButton"]'; +export const MANAGE_ALERT_DETECTION_RULES_BTN = '[data-test-subj="manage-alert-detection-rules"]'; -export const CLOSE_SELECTED_ALERTS_BTN = '[data-test-subj="closeSelectedAlertsButton"]'; +export const MARK_ALERT_IN_PROGRESS_BTN = '[data-test-subj="in-progress-alert-status"]'; export const MARK_SELECTED_ALERTS_IN_PROGRESS_BTN = '[data-test-subj="markSelectedAlertsInProgressButton"]'; +export const NUMBER_OF_ALERTS = '[data-test-subj="server-side-event-count"] .euiBadge__text'; + export const OPEN_ALERT_BTN = '[data-test-subj="open-alert-status"]'; -export const CLOSE_ALERT_BTN = '[data-test-subj="close-alert-status"]'; +export const OPEN_SELECTED_ALERTS_BTN = '[data-test-subj="openSelectedAlertsButton"]'; -export const MARK_ALERT_IN_PROGRESS_BTN = '[data-test-subj="in-progress-alert-status"]'; +export const OPENED_ALERTS_FILTER_BTN = '[data-test-subj="openAlerts"]'; + +export const SELECTED_ALERTS = '[data-test-subj="selectedAlerts"]'; + +export const SEND_ALERT_TO_TIMELINE_BTN = '[data-test-subj="send-alert-to-timeline-button"]'; + +export const SHOWING_ALERTS = '[data-test-subj="showingAlerts"]'; + +export const TAKE_ACTION_POPOVER_BTN = '[data-test-subj="alertActionPopover"] button'; + +export const TIMELINE_CONTEXT_MENU_BTN = '[data-test-subj="timeline-context-menu-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 dda371126d5aa..e1ab5ff30572f 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 @@ -57,6 +57,12 @@ export const INVESTIGATION_NOTES_TEXTAREA = export const FALSE_POSITIVES_INPUT = '[data-test-subj="detectionEngineStepAboutRuleFalsePositives"] input'; +export const LOOK_BACK_INTERVAL = + '[data-test-subj="detectionEngineStepScheduleRuleFrom"] [data-test-subj="interval"]'; + +export const LOOK_BACK_TIME_TYPE = + '[data-test-subj="detectionEngineStepScheduleRuleFrom"] [data-test-subj="timeType"]'; + export const MACHINE_LEARNING_DROPDOWN = '[data-test-subj="mlJobSelect"] button'; export const MACHINE_LEARNING_LIST = '.euiContextMenuItem__text'; @@ -73,6 +79,8 @@ export const MITRE_TECHNIQUES_INPUT = export const REFERENCE_URLS_INPUT = '[data-test-subj="detectionEngineStepAboutRuleReferenceUrls"] input'; +export const REFRESH_BUTTON = '[data-test-subj="refreshButton"]'; + export const RISK_INPUT = '.euiRangeInput'; export const RISK_MAPPING_OVERRIDE_OPTION = '#risk_score-mapping-override'; @@ -88,21 +96,29 @@ export const RULE_NAME_INPUT = export const RULE_NAME_OVERRIDE = '[data-test-subj="detectionEngineStepAboutRuleRuleNameOverride"]'; +export const RULE_STATUS = '[data-test-subj="ruleStatus"]'; + export const RULE_TIMESTAMP_OVERRIDE = '[data-test-subj="detectionEngineStepAboutRuleTimestampOverride"]'; +export const RUNS_EVERY_INTERVAL = + '[data-test-subj="detectionEngineStepScheduleRuleInterval"] [data-test-subj="interval"]'; + +export const RUNS_EVERY_TIME_TYPE = + '[data-test-subj="detectionEngineStepScheduleRuleInterval"] [data-test-subj="timeType"]'; + 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"]'; + '[data-test-subj="detectionEngineStepScheduleRuleInterval"] [data-test-subj="interval"]'; export const SCHEDULE_INTERVAL_UNITS_INPUT = - '[data-test-subj="detectionEngineStepScheduleRuleInterval"] [data-test-subj="schedule-units-input"]'; + '[data-test-subj="detectionEngineStepScheduleRuleInterval"] [data-test-subj="timeType"]'; export const SCHEDULE_LOOKBACK_AMOUNT_INPUT = - '[data-test-subj="detectionEngineStepScheduleRuleFrom"] [data-test-subj="schedule-amount-input"]'; + '[data-test-subj="detectionEngineStepScheduleRuleFrom"] [data-test-subj="timeType"]'; export const SCHEDULE_LOOKBACK_UNITS_INPUT = '[data-test-subj="detectionEngineStepScheduleRuleFrom"] [data-test-subj="schedule-units-input"]'; 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 98fc7b06a9908..5a376e95e38dd 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -4,12 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export const getDescriptionForTitle = (title: string) => - cy.get(DETAILS_TITLE).contains(title).next(DETAILS_DESCRIPTION); - -export const DETAILS_DESCRIPTION = '.euiDescriptionList__description'; -export const DETAILS_TITLE = '.euiDescriptionList__title'; - export const ABOUT_INVESTIGATION_NOTES = '[data-test-subj="stepAboutDetailsNoteContent"]'; export const ABOUT_RULE_DESCRIPTION = '[data-test-subj=stepAboutRuleDetailsToggleDescriptionText]'; @@ -17,9 +11,23 @@ export const ABOUT_RULE_DESCRIPTION = '[data-test-subj=stepAboutRuleDetailsToggl export const ABOUT_DETAILS = '[data-test-subj="aboutRule"] [data-test-subj="listItemColumnStepRuleDescription"]'; +export const ADDITIONAL_LOOK_BACK_DETAILS = 'Additional look-back time'; + +export const ANOMALY_SCORE_DETAILS = 'Anomaly score'; + +export const CUSTOM_QUERY_DETAILS = 'Custom query'; + export const DEFINITION_DETAILS = '[data-test-subj=definitionRule] [data-test-subj="listItemColumnStepRuleDescription"]'; +export const DETAILS_DESCRIPTION = '.euiDescriptionList__description'; + +export const DETAILS_TITLE = '.euiDescriptionList__title'; + +export const FALSE_POSITIVES_DETAILS = 'False positive examples'; + +export const INDEX_PATTERNS_DETAILS = 'Index patterns'; + export const INVESTIGATION_NOTES_MARKDOWN = 'test markdown'; export const INVESTIGATION_NOTES_TOGGLE = 1; @@ -28,11 +36,38 @@ export const MACHINE_LEARNING_JOB_ID = '[data-test-subj="machineLearningJobId"]' export const MACHINE_LEARNING_JOB_STATUS = '[data-test-subj="machineLearningJobStatus"]'; +export const MITRE_ATTACK_DETAILS = 'MITRE ATT&CK'; + 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_NAME_OVERRIDE_DETAILS = 'Rule name override'; + +export const RISK_SCORE_DETAILS = 'Risk score'; + +export const RISK_SCORE_OVERRIDE_DETAILS = 'Risk score override'; + +export const REFERENCE_URLS_DETAILS = 'Reference URLs'; + +export const RULE_TYPE_DETAILS = 'Rule type'; + +export const RUNS_EVERY_DETAILS = 'Runs every'; + export const SCHEDULE_DETAILS = '[data-test-subj=schedule] [data-test-subj="listItemColumnStepRuleDescription"]'; export const SCHEDULE_STEP = '[data-test-subj="schedule"] .euiDescriptionList__description'; + +export const SEVERITY_DETAILS = 'Severity'; + +export const TAGS_DETAILS = 'Tags'; + +export const THRESHOLD_DETAILS = 'Threshold'; + +export const TIMELINE_TEMPLATE_DETAILS = 'Timeline template'; + +export const TIMESTAMP_OVERRIDE_DETAILS = 'Timestamp override'; + +export const getDetails = (title: string) => + cy.get(DETAILS_TITLE).contains(title).next(DETAILS_DESCRIPTION); diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts index f7ef5a904de99..c846ced2febfd 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts @@ -22,8 +22,10 @@ import { OPEN_SELECTED_ALERTS_BTN, MARK_ALERT_IN_PROGRESS_BTN, MARK_SELECTED_ALERTS_IN_PROGRESS_BTN, + ALERT_RISK_SCORE_HEADER, } from '../screens/alerts'; import { REFRESH_BUTTON } from '../screens/security_header'; +import { TIMELINE_COLUMN_SPINNER } from '../screens/timeline'; export const closeFirstAlert = () => { cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click({ force: true }); @@ -81,6 +83,12 @@ export const selectNumberOfAlerts = (numberOfAlerts: number) => { } }; +export const sortRiskScore = () => { + cy.get(ALERT_RISK_SCORE_HEADER).click(); + cy.get(TIMELINE_COLUMN_SPINNER).should('exist'); + cy.get(TIMELINE_COLUMN_SPINNER).should('not.exist'); +}; + export const investigateFirstAlertInTimeline = () => { cy.get(SEND_ALERT_TO_TIMELINE_BTN).first().click({ force: true }); }; 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 dc89a39d082bc..914566a13a9a9 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 @@ -28,6 +28,8 @@ import { IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK, INPUT, INVESTIGATION_NOTES_TEXTAREA, + LOOK_BACK_INTERVAL, + LOOK_BACK_TIME_TYPE, MACHINE_LEARNING_DROPDOWN, MACHINE_LEARNING_LIST, MACHINE_LEARNING_TYPE, @@ -36,13 +38,17 @@ import { MITRE_TACTIC_DROPDOWN, MITRE_TECHNIQUES_INPUT, REFERENCE_URLS_INPUT, + REFRESH_BUTTON, RISK_INPUT, RISK_MAPPING_OVERRIDE_OPTION, RISK_OVERRIDE, RULE_DESCRIPTION_INPUT, RULE_NAME_INPUT, RULE_NAME_OVERRIDE, + RULE_STATUS, RULE_TIMESTAMP_OVERRIDE, + RUNS_EVERY_INTERVAL, + RUNS_EVERY_TIME_TYPE, SCHEDULE_CONTINUE_BUTTON, SCHEDULE_EDIT_TAB, SEVERITY_DROPDOWN, @@ -190,6 +196,13 @@ export const fillDefineCustomRuleWithImportedQueryAndContinue = ( cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); }; +export const fillScheduleRuleAndContinue = (rule: CustomRule | MachineLearningRule) => { + cy.get(RUNS_EVERY_INTERVAL).clear().type(rule.runsEvery.interval); + cy.get(RUNS_EVERY_TIME_TYPE).select(rule.runsEvery.timeType); + cy.get(LOOK_BACK_INTERVAL).clear().type(rule.lookBack.interval); + cy.get(LOOK_BACK_TIME_TYPE).select(rule.lookBack.timeType); +}; + export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { const thresholdField = 0; const threshold = 1; @@ -251,6 +264,14 @@ export const selectThresholdRuleType = () => { cy.get(THRESHOLD_TYPE).click({ force: true }); }; +export const waitForTheRuleToBeExecuted = async () => { + let status = ''; + while (status !== 'succeeded') { + cy.get(REFRESH_BUTTON).click(); + status = await cy.get(RULE_STATUS).invoke('text').promisify(); + } +}; + export const selectEqlRuleType = () => { cy.get(EQL_TYPE).click({ force: true }); }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/security_header.ts b/x-pack/plugins/security_solution/cypress/tasks/security_header.ts index 7427104a9d889..28efc47120d32 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/security_header.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/security_header.ts @@ -21,3 +21,7 @@ export const navigateFromHeaderTo = (page: string) => { export const refreshPage = () => { cy.get(REFRESH_BUTTON).click({ force: true }).invoke('text').should('not.equal', 'Updating'); }; + +export const waitForThePageToBeUpdated = () => { + cy.get(REFRESH_BUTTON).should('not.equal', 'Updating'); +}; diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 6d79557fdaa28..4c9e3bc06037e 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -11,7 +11,7 @@ "cypress:open": "cypress open --config-file ./cypress/cypress.json", "cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/visual_config.ts", "cypress:run": "cypress run --browser chrome --headless --spec ./cypress/integration/**/*.spec.ts --config-file ./cypress/cypress.json --reporter ../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json; status=$?; ../../node_modules/.bin/mochawesome-merge ../../../target/kibana-security-solution/cypress/results/mochawesome*.json > ../../../target/kibana-security-solution/cypress/results/output.json; ../../../node_modules/.bin/marge ../../../target/kibana-security-solution/cypress/results/output.json --reportDir ../../../target/kibana-security-solution/cypress/results; mkdir -p ../../../target/junit && cp ../../../target/kibana-security-solution/cypress/results/*.xml ../../../target/junit/ && exit $status;", - "cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/cli_config.ts", + "cypress:run-as-ci": "node --max-old-space-size=2048 ../../../scripts/functional_tests --config ../../test/security_solution_cypress/cli_config.ts", "test:generate": "node scripts/endpoint/resolver_generator" }, "devDependencies": { diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index 24e25470feb3b..68eb93f7e2fe8 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -21,6 +21,7 @@ import { useInitSourcerer, useSourcererScope } from '../../common/containers/sou import { useKibana } from '../../common/lib/kibana'; import { DETECTIONS_SUB_PLUGIN_ID } from '../../../common/constants'; import { SourcererScopeName } from '../../common/store/sourcerer/model'; +import { useUpgradeEndpointPackage } from '../../common/hooks/endpoint/upgrade'; import { useThrottledResizeObserver } from '../../common/components/utils'; const Main = styled.main.attrs<{ paddingTop: number }>(({ paddingTop }) => ({ @@ -58,6 +59,12 @@ const HomePageComponent: React.FC = ({ children }) => { const [showTimeline] = useShowTimeline(); const { browserFields, indexPattern, indicesExist } = useSourcererScope(); + // side effect: this will attempt to upgrade the endpoint package if it is not up to date + // this will run when a user navigates to the Security Solution app and when they navigate between + // tabs in the app. This is useful for keeping the endpoint package as up to date as possible until + // a background task solution can be built on the server side. Once a background task solution is available we + // can remove this. + useUpgradeEndpointPackage(); return ( 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 a800bd690f710..a85d7a310bc06 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 @@ -17,8 +17,7 @@ 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'; +import { waitFor } from '@testing-library/react'; jest.mock( '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx index e6e0823214195..e301e80c9561d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount } from 'enzyme'; import moment from 'moment-timezone'; - +import { waitFor } from '@testing-library/react'; import '../../../common/mock/match_media'; import { AllCases } from '.'; import { TestProviders } from '../../../common/mock'; @@ -85,16 +85,6 @@ describe('AllCases', () => { let navigateToApp: jest.Mock; - /* eslint-disable no-console */ - // Silence until enzyme fixed to use ReactTestUtils.act() - const originalError = console.error; - beforeAll(() => { - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - }); - /* eslint-enable no-console */ beforeEach(() => { jest.clearAllMocks(); navigateToApp = jest.fn(); @@ -106,36 +96,38 @@ describe('AllCases', () => { moment.tz.setDefault('UTC'); }); - it('should render AllCases', () => { + it('should render AllCases', async () => { const wrapper = mount( ); - expect(wrapper.find(`a[data-test-subj="case-details-link"]`).first().prop('href')).toEqual( - `/${useGetCasesMockState.data.cases[0].id}` - ); - expect(wrapper.find(`a[data-test-subj="case-details-link"]`).first().text()).toEqual( - useGetCasesMockState.data.cases[0].title - ); - expect( - wrapper.find(`span[data-test-subj="case-table-column-tags-0"]`).first().prop('title') - ).toEqual(useGetCasesMockState.data.cases[0].tags[0]); - expect(wrapper.find(`[data-test-subj="case-table-column-createdBy"]`).first().text()).toEqual( - useGetCasesMockState.data.cases[0].createdBy.fullName - ); - expect( - wrapper - .find(`[data-test-subj="case-table-column-createdAt"]`) - .first() - .childAt(0) - .prop('value') - ).toBe(useGetCasesMockState.data.cases[0].createdAt); - expect(wrapper.find(`[data-test-subj="case-table-case-count"]`).first().text()).toEqual( - 'Showing 10 cases' - ); + await waitFor(() => { + expect(wrapper.find(`a[data-test-subj="case-details-link"]`).first().prop('href')).toEqual( + `/${useGetCasesMockState.data.cases[0].id}` + ); + expect(wrapper.find(`a[data-test-subj="case-details-link"]`).first().text()).toEqual( + useGetCasesMockState.data.cases[0].title + ); + expect( + wrapper.find(`span[data-test-subj="case-table-column-tags-0"]`).first().prop('title') + ).toEqual(useGetCasesMockState.data.cases[0].tags[0]); + expect(wrapper.find(`[data-test-subj="case-table-column-createdBy"]`).first().text()).toEqual( + useGetCasesMockState.data.cases[0].createdBy.fullName + ); + expect( + wrapper + .find(`[data-test-subj="case-table-column-createdAt"]`) + .first() + .childAt(0) + .prop('value') + ).toBe(useGetCasesMockState.data.cases[0].createdAt); + expect(wrapper.find(`[data-test-subj="case-table-case-count"]`).first().text()).toEqual( + 'Showing 10 cases' + ); + }); }); - it('should render empty fields', () => { + it('should render empty fields', async () => { useGetCasesMock.mockReturnValue({ ...defaultGetCases, data: { @@ -166,53 +158,63 @@ describe('AllCases', () => { expect(column.find('.euiTableRowCell--hideForDesktop').text()).toEqual(columnName); expect(column.find('span').text()).toEqual(emptyTag); }; - getCasesColumns([], 'open', false).map((i, key) => i.name != null && checkIt(`${i.name}`, key)); + await waitFor(() => { + getCasesColumns([], 'open', false).map( + (i, key) => i.name != null && checkIt(`${i.name}`, key) + ); + }); }); - it('should not render case link or actions on modal=true', () => { + it('should not render case link or actions on modal=true', async () => { const wrapper = mount( ); - const checkIt = (columnName: string) => { - expect(columnName).not.toEqual(i18n.ACTIONS); - }; - getCasesColumns([], 'open', true).map((i, key) => i.name != null && checkIt(`${i.name}`)); - expect(wrapper.find(`a[data-test-subj="case-details-link"]`).exists()).toBeFalsy(); + await waitFor(() => { + const checkIt = (columnName: string) => { + expect(columnName).not.toEqual(i18n.ACTIONS); + }; + getCasesColumns([], 'open', true).map((i, key) => i.name != null && checkIt(`${i.name}`)); + expect(wrapper.find(`a[data-test-subj="case-details-link"]`).exists()).toBeFalsy(); + }); }); - it('should tableHeaderSortButton AllCases', () => { + it('should tableHeaderSortButton AllCases', async () => { const wrapper = mount( ); - wrapper.find('[data-test-subj="tableHeaderSortButton"]').first().simulate('click'); - expect(setQueryParams).toBeCalledWith({ - page: 1, - perPage: 5, - sortField: 'createdAt', - sortOrder: 'asc', + await waitFor(() => { + wrapper.find('[data-test-subj="tableHeaderSortButton"]').first().simulate('click'); + expect(setQueryParams).toBeCalledWith({ + page: 1, + perPage: 5, + sortField: 'createdAt', + sortOrder: 'asc', + }); }); }); - it('closes case when row action icon clicked', () => { + it('closes case when row action icon clicked', async () => { const wrapper = mount( ); - wrapper.find('[data-test-subj="action-close"]').first().simulate('click'); - const firstCase = useGetCasesMockState.data.cases[0]; - expect(dispatchUpdateCaseProperty).toBeCalledWith({ - caseId: firstCase.id, - updateKey: 'status', - updateValue: 'closed', - refetchCasesStatus: fetchCasesStatus, - version: firstCase.version, + await waitFor(() => { + wrapper.find('[data-test-subj="action-close"]').first().simulate('click'); + const firstCase = useGetCasesMockState.data.cases[0]; + expect(dispatchUpdateCaseProperty).toBeCalledWith({ + caseId: firstCase.id, + updateKey: 'status', + updateValue: 'closed', + refetchCasesStatus: fetchCasesStatus, + version: firstCase.version, + }); }); }); - it('opens case when row action icon clicked', () => { + it('opens case when row action icon clicked', async () => { useGetCasesMock.mockReturnValue({ ...defaultGetCases, filterOptions: { ...defaultGetCases.filterOptions, status: 'closed' }, @@ -223,17 +225,19 @@ describe('AllCases', () => { ); - wrapper.find('[data-test-subj="action-open"]').first().simulate('click'); - const firstCase = useGetCasesMockState.data.cases[0]; - expect(dispatchUpdateCaseProperty).toBeCalledWith({ - caseId: firstCase.id, - updateKey: 'status', - updateValue: 'open', - refetchCasesStatus: fetchCasesStatus, - version: firstCase.version, + await waitFor(() => { + wrapper.find('[data-test-subj="action-open"]').first().simulate('click'); + const firstCase = useGetCasesMockState.data.cases[0]; + expect(dispatchUpdateCaseProperty).toBeCalledWith({ + caseId: firstCase.id, + updateKey: 'status', + updateValue: 'open', + refetchCasesStatus: fetchCasesStatus, + version: firstCase.version, + }); }); }); - it('Bulk delete', () => { + it('Bulk delete', async () => { useGetCasesMock.mockReturnValue({ ...defaultGetCases, selectedCases: useGetCasesMockState.data.cases, @@ -254,21 +258,23 @@ describe('AllCases', () => { ); - wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); - wrapper.find('[data-test-subj="cases-bulk-delete-button"]').first().simulate('click'); - expect(handleToggleModal).toBeCalled(); + await waitFor(() => { + wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); + wrapper.find('[data-test-subj="cases-bulk-delete-button"]').first().simulate('click'); + expect(handleToggleModal).toBeCalled(); - wrapper - .find( - '[data-test-subj="confirm-delete-case-modal"] [data-test-subj="confirmModalConfirmButton"]' - ) - .last() - .simulate('click'); - expect(handleOnDeleteConfirm.mock.calls[0][0]).toStrictEqual( - useGetCasesMockState.data.cases.map(({ id }) => ({ id })) - ); + wrapper + .find( + '[data-test-subj="confirm-delete-case-modal"] [data-test-subj="confirmModalConfirmButton"]' + ) + .last() + .simulate('click'); + expect(handleOnDeleteConfirm.mock.calls[0][0]).toStrictEqual( + useGetCasesMockState.data.cases.map(({ id }) => ({ id })) + ); + }); }); - it('Bulk close status update', () => { + it('Bulk close status update', async () => { useGetCasesMock.mockReturnValue({ ...defaultGetCases, selectedCases: useGetCasesMockState.data.cases, @@ -279,11 +285,13 @@ describe('AllCases', () => { ); - wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); - wrapper.find('[data-test-subj="cases-bulk-close-button"]').first().simulate('click'); - expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, 'closed'); + await waitFor(() => { + wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); + wrapper.find('[data-test-subj="cases-bulk-close-button"]').first().simulate('click'); + expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, 'closed'); + }); }); - it('Bulk open status update', () => { + it('Bulk open status update', async () => { useGetCasesMock.mockReturnValue({ ...defaultGetCases, selectedCases: useGetCasesMockState.data.cases, @@ -298,11 +306,13 @@ describe('AllCases', () => { ); - wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); - wrapper.find('[data-test-subj="cases-bulk-open-button"]').first().simulate('click'); - expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, 'open'); + await waitFor(() => { + wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); + wrapper.find('[data-test-subj="cases-bulk-open-button"]').first().simulate('click'); + expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, 'open'); + }); }); - it('isDeleted is true, refetch', () => { + it('isDeleted is true, refetch', async () => { useDeleteCasesMock.mockReturnValue({ ...defaultDeleteCases, isDeleted: true, @@ -313,11 +323,13 @@ describe('AllCases', () => { ); - expect(refetchCases).toBeCalled(); - expect(fetchCasesStatus).toBeCalled(); - expect(dispatchResetIsDeleted).toBeCalled(); + await waitFor(() => { + expect(refetchCases).toBeCalled(); + expect(fetchCasesStatus).toBeCalled(); + expect(dispatchResetIsDeleted).toBeCalled(); + }); }); - it('isUpdated is true, refetch', () => { + it('isUpdated is true, refetch', async () => { useUpdateCasesMock.mockReturnValue({ ...defaultUpdateCases, isUpdated: true, @@ -328,42 +340,51 @@ describe('AllCases', () => { ); - expect(refetchCases).toBeCalled(); - expect(fetchCasesStatus).toBeCalled(); - expect(dispatchResetIsUpdated).toBeCalled(); + await waitFor(() => { + expect(refetchCases).toBeCalled(); + expect(fetchCasesStatus).toBeCalled(); + expect(dispatchResetIsUpdated).toBeCalled(); + }); }); - it('should not render header when modal=true', () => { + it('should not render header when modal=true', async () => { const wrapper = mount( ); - - expect(wrapper.find('[data-test-subj="all-cases-header"]').exists()).toBe(false); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="all-cases-header"]').exists()).toBe(false); + }); }); - it('should not render table utility bar when modal=true', () => { + it('should not render table utility bar when modal=true', async () => { const wrapper = mount( ); - - expect(wrapper.find('[data-test-subj="case-table-utility-bar-actions"]').exists()).toBe(false); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="case-table-utility-bar-actions"]').exists()).toBe( + false + ); + }); }); - it('case table should not be selectable when modal=true', () => { + it('case table should not be selectable when modal=true', async () => { const wrapper = mount( ); - - expect(wrapper.find('[data-test-subj="cases-table"]').first().prop('isSelectable')).toBe(false); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="cases-table"]').first().prop('isSelectable')).toBe( + false + ); + }); }); - it('should call onRowClick with no cases and modal=true', () => { + it('should call onRowClick with no cases and modal=true', async () => { useGetCasesMock.mockReturnValue({ ...defaultGetCases, data: { @@ -378,12 +399,13 @@ describe('AllCases', () => { ); - - wrapper.find('[data-test-subj="cases-table-add-case"]').first().simulate('click'); - expect(onRowClick).toHaveBeenCalled(); + await waitFor(() => { + wrapper.find('[data-test-subj="cases-table-add-case"]').first().simulate('click'); + expect(onRowClick).toHaveBeenCalled(); + }); }); - it('should call navigateToApp with no cases and modal=false', () => { + it('should call navigateToApp with no cases and modal=false', async () => { useGetCasesMock.mockReturnValue({ ...defaultGetCases, data: { @@ -398,30 +420,33 @@ describe('AllCases', () => { ); - - wrapper.find('[data-test-subj="cases-table-add-case"]').first().simulate('click'); - expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/create' }); + await waitFor(() => { + wrapper.find('[data-test-subj="cases-table-add-case"]').first().simulate('click'); + expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/create' }); + }); }); - it('should call onRowClick when clicking a case with modal=true', () => { + it('should call onRowClick when clicking a case with modal=true', async () => { const wrapper = mount( ); - - wrapper.find('[data-test-subj="cases-table-row-1"]').first().simulate('click'); - expect(onRowClick).toHaveBeenCalledWith('1'); + await waitFor(() => { + wrapper.find('[data-test-subj="cases-table-row-1"]').first().simulate('click'); + expect(onRowClick).toHaveBeenCalledWith('1'); + }); }); - it('should NOT call onRowClick when clicking a case with modal=true', () => { + it('should NOT call onRowClick when clicking a case with modal=true', async () => { const wrapper = mount( ); - - wrapper.find('[data-test-subj="cases-table-row-1"]').first().simulate('click'); - expect(onRowClick).not.toHaveBeenCalled(); + await waitFor(() => { + wrapper.find('[data-test-subj="cases-table-row-1"]').first().simulate('click'); + expect(onRowClick).not.toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.test.tsx index b93de014f5c18..725759068a3ea 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.test.tsx @@ -5,6 +5,7 @@ */ import { mount } from 'enzyme'; import React from 'react'; +import { waitFor } from '@testing-library/react'; import '../../../common/mock/match_media'; import { AllCasesModal } from '.'; import { TestProviders } from '../../../common/mock'; @@ -96,16 +97,6 @@ describe('AllCasesModal', () => { dispatchResetIsUpdated, updateBulkStatus, }; - /* eslint-disable no-console */ - // Silence until enzyme fixed to use ReactTestUtils.act() - const originalError = console.error; - beforeAll(() => { - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - }); - /* eslint-enable no-console */ beforeEach(() => { jest.resetAllMocks(); useUpdateCasesMock.mockImplementation(() => defaultUpdateCases); @@ -114,41 +105,49 @@ describe('AllCasesModal', () => { useGetCasesStatusMock.mockImplementation(() => defaultCasesStatus); }); - it('renders with unselectable rows', () => { + it('renders with unselectable rows', async () => { const wrapper = mount( ); - expect(wrapper.find(`[data-test-subj='all-cases-modal']`).exists()).toBeTruthy(); - expect(wrapper.find(EuiTableRow).first().prop('isSelectable')).toBeFalsy(); + await waitFor(() => { + expect(wrapper.find(`[data-test-subj='all-cases-modal']`).exists()).toBeTruthy(); + expect(wrapper.find(EuiTableRow).first().prop('isSelectable')).toBeFalsy(); + }); }); - it('does not render modal if showCaseModal: false', () => { + it('does not render modal if showCaseModal: false', async () => { const wrapper = mount( ); - expect(wrapper.find(`[data-test-subj='all-cases-modal']`).exists()).toBeFalsy(); + await waitFor(() => { + expect(wrapper.find(`[data-test-subj='all-cases-modal']`).exists()).toBeFalsy(); + }); }); - it('onRowClick called when row is clicked', () => { + it('onRowClick called when row is clicked', async () => { const wrapper = mount( ); - const firstRow = wrapper.find(EuiTableRow).first(); - firstRow.simulate('click'); - expect(onRowClick.mock.calls[0][0]).toEqual(basicCaseId); + await waitFor(() => { + const firstRow = wrapper.find(EuiTableRow).first(); + firstRow.simulate('click'); + expect(onRowClick.mock.calls[0][0]).toEqual(basicCaseId); + }); }); - it('Closing modal calls onCloseCaseModal', () => { + it('Closing modal calls onCloseCaseModal', async () => { const wrapper = mount( ); - const modalClose = wrapper.find('.euiModal__closeIcon').first(); - modalClose.simulate('click'); - expect(onCloseCaseModal).toBeCalled(); + await waitFor(() => { + const modalClose = wrapper.find('.euiModal__closeIcon').first(); + modalClose.simulate('click'); + expect(onCloseCaseModal).toBeCalled(); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx index 246df1c94b817..3859b4527991b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx @@ -15,9 +15,7 @@ import { TestProviders } from '../../../common/mock'; import { useUpdateCase } from '../../containers/use_update_case'; import { useGetCase } from '../../containers/use_get_case'; import { useGetCaseUserActions } from '../../containers/use_get_case_user_actions'; - -// 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'; +import { waitFor } from '@testing-library/react'; import { useConnectors } from '../../containers/configure/use_connectors'; import { connectorsMock } from '../../containers/configure/mock'; @@ -29,6 +27,7 @@ jest.mock('../../containers/use_get_case_user_actions'); jest.mock('../../containers/use_get_case'); jest.mock('../../containers/configure/use_connectors'); jest.mock('../../containers/use_post_push_to_service'); +jest.mock('../user_action_tree/user_action_timestamp'); const useUpdateCaseMock = useUpdateCase as jest.Mock; const useGetCaseUserActionsMock = useGetCaseUserActions as jest.Mock; @@ -63,16 +62,6 @@ describe('CaseView ', () => { updateCase, fetchCase, }; - /* eslint-disable no-console */ - // Silence until enzyme fixed to use ReactTestUtils.act() - const originalError = console.error; - beforeAll(() => { - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - }); - /* eslint-enable no-console */ const defaultUpdateCaseState = { isLoading: false, @@ -96,6 +85,7 @@ describe('CaseView ', () => { beforeEach(() => { jest.resetAllMocks(); useUpdateCaseMock.mockImplementation(() => defaultUpdateCaseState); + jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); useGetCaseUserActionsMock.mockImplementation(() => defaultUseGetCaseUserActions); usePostPushToServiceMock.mockImplementation(() => ({ isLoading: false, postPushToService })); @@ -191,7 +181,7 @@ describe('CaseView ', () => { }); }); - it('should display EditableTitle isLoading', () => { + it('should display EditableTitle isLoading', async () => { useUpdateCaseMock.mockImplementation(() => ({ ...defaultUpdateCaseState, isLoading: true, @@ -204,13 +194,17 @@ describe('CaseView ', () => { ); - expect(wrapper.find('[data-test-subj="editable-title-loading"]').first().exists()).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="editable-title-edit-icon"]').first().exists() - ).toBeFalsy(); + await waitFor(() => { + expect( + wrapper.find('[data-test-subj="editable-title-loading"]').first().exists() + ).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="editable-title-edit-icon"]').first().exists() + ).toBeFalsy(); + }); }); - it('should display Toggle Status isLoading', () => { + it('should display Toggle Status isLoading', async () => { useUpdateCaseMock.mockImplementation(() => ({ ...defaultUpdateCaseState, isLoading: true, @@ -223,12 +217,14 @@ describe('CaseView ', () => { ); - expect( - wrapper.find('[data-test-subj="toggle-case-status"]').first().prop('isLoading') - ).toBeTruthy(); + await waitFor(() => { + expect( + wrapper.find('[data-test-subj="toggle-case-status"]').first().prop('isLoading') + ).toBeTruthy(); + }); }); - it('should display description isLoading', () => { + it('should display description isLoading', async () => { useUpdateCaseMock.mockImplementation(() => ({ ...defaultUpdateCaseState, isLoading: true, @@ -241,21 +237,25 @@ describe('CaseView ', () => { ); - expect( - wrapper - .find('[data-test-subj="description-action"] [data-test-subj="user-action-title-loading"]') - .first() - .exists() - ).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="description-action"] [data-test-subj="property-actions"]') - .first() - .exists() - ).toBeFalsy(); + await waitFor(() => { + expect( + wrapper + .find( + '[data-test-subj="description-action"] [data-test-subj="user-action-title-loading"]' + ) + .first() + .exists() + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="description-action"] [data-test-subj="property-actions"]') + .first() + .exists() + ).toBeFalsy(); + }); }); - it('should display tags isLoading', () => { + it('should display tags isLoading', async () => { useUpdateCaseMock.mockImplementation(() => ({ ...defaultUpdateCaseState, isLoading: true, @@ -268,16 +268,18 @@ describe('CaseView ', () => { ); - expect( - wrapper - .find('[data-test-subj="case-view-tag-list"] [data-test-subj="tag-list-loading"]') - .first() - .exists() - ).toBeTruthy(); - expect(wrapper.find('[data-test-subj="tag-list-edit"]').first().exists()).toBeFalsy(); + await waitFor(() => { + expect( + wrapper + .find('[data-test-subj="case-view-tag-list"] [data-test-subj="tag-list-loading"]') + .first() + .exists() + ).toBeTruthy(); + expect(wrapper.find('[data-test-subj="tag-list-edit"]').first().exists()).toBeFalsy(); + }); }); - it('should update title', () => { + it('should update title', async () => { const wrapper = mount( @@ -285,21 +287,23 @@ describe('CaseView ', () => { ); - const newTitle = 'The new title'; - wrapper.find(`[data-test-subj="editable-title-edit-icon"]`).first().simulate('click'); - wrapper.update(); - wrapper - .find(`[data-test-subj="editable-title-input-field"]`) - .last() - .simulate('change', { target: { value: newTitle } }); - - wrapper.update(); - wrapper.find(`[data-test-subj="editable-title-submit-btn"]`).first().simulate('click'); - - wrapper.update(); - const updateObject = updateCaseProperty.mock.calls[0][0]; - expect(updateObject.updateKey).toEqual('title'); - expect(updateObject.updateValue).toEqual(newTitle); + await waitFor(() => { + const newTitle = 'The new title'; + wrapper.find(`[data-test-subj="editable-title-edit-icon"]`).first().simulate('click'); + wrapper.update(); + wrapper + .find(`[data-test-subj="editable-title-input-field"]`) + .last() + .simulate('change', { target: { value: newTitle } }); + + wrapper.update(); + wrapper.find(`[data-test-subj="editable-title-submit-btn"]`).first().simulate('click'); + + wrapper.update(); + const updateObject = updateCaseProperty.mock.calls[0][0]; + expect(updateObject.updateKey).toEqual('title'); + expect(updateObject.updateValue).toEqual(newTitle); + }); }); it('should push updates on button click', async () => { @@ -329,7 +333,7 @@ describe('CaseView ', () => { }); }); - it('should return null if error', () => { + it('should return null if error', async () => { (useGetCase as jest.Mock).mockImplementation(() => ({ ...defaultGetCase, isError: true, @@ -346,10 +350,12 @@ describe('CaseView ', () => { ); - expect(wrapper).toEqual({}); + await waitFor(() => { + expect(wrapper).toEqual({}); + }); }); - it('should return spinner if loading', () => { + it('should return spinner if loading', async () => { (useGetCase as jest.Mock).mockImplementation(() => ({ ...defaultGetCase, isLoading: true, @@ -366,10 +372,12 @@ describe('CaseView ', () => { ); - expect(wrapper.find('[data-test-subj="case-view-loading"]').exists()).toBeTruthy(); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="case-view-loading"]').exists()).toBeTruthy(); + }); }); - it('should return case view when data is there', () => { + it('should return case view when data is there', async () => { (useGetCase as jest.Mock).mockImplementation(() => defaultGetCase); const wrapper = mount( @@ -383,10 +391,12 @@ describe('CaseView ', () => { ); - expect(wrapper.find('[data-test-subj="case-view-title"]').exists()).toBeTruthy(); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="case-view-title"]').exists()).toBeTruthy(); + }); }); - it('should refresh data on refresh', () => { + it('should refresh data on refresh', async () => { (useGetCase as jest.Mock).mockImplementation(() => defaultGetCase); const wrapper = mount( @@ -400,12 +410,14 @@ describe('CaseView ', () => { ); - wrapper.find('[data-test-subj="case-refresh"]').first().simulate('click'); - expect(fetchCaseUserActions).toBeCalledWith(caseProps.caseData.id); - expect(fetchCase).toBeCalled(); + await waitFor(() => { + wrapper.find('[data-test-subj="case-refresh"]').first().simulate('click'); + expect(fetchCaseUserActions).toBeCalledWith(caseProps.caseData.id); + expect(fetchCase).toBeCalled(); + }); }); - it('should disable the push button when connector is invalid', () => { + it('should disable the push button when connector is invalid', async () => { useGetCaseUserActionsMock.mockImplementation(() => ({ ...defaultUseGetCaseUserActions, hasDataToPush: true, @@ -424,10 +436,11 @@ describe('CaseView ', () => { ); - - expect( - wrapper.find('button[data-test-subj="push-to-external-service"]').first().prop('disabled') - ).toBeTruthy(); + await waitFor(() => { + expect( + wrapper.find('button[data-test-subj="push-to-external-service"]').first().prop('disabled') + ).toBeTruthy(); + }); }); it('should revert to the initial connector in case of failure', async () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx index 4f1e45ae7c115..d27f00aacff2c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { mount } from 'enzyme'; - import { Create } from '.'; import { TestProviders } from '../../../common/mock'; import { getFormMock } from '../__mock__/form'; @@ -19,9 +18,16 @@ import { useGetTags } from '../../containers/use_get_tags'; 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'; +import { waitFor } from '@testing-library/react'; +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + return { + ...original, + // eslint-disable-next-line react/display-name + EuiFieldText: () => , + }; +}); jest.mock('../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'); jest.mock('../../containers/use_post_case'); @@ -74,16 +80,6 @@ const defaultPostCase = { postCase, }; describe('Create case', () => { - // Suppress warnings about "noSuggestions" prop - /* eslint-disable no-console */ - const originalError = console.error; - beforeAll(() => { - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - }); - /* eslint-enable no-console */ const fetchTags = jest.fn(); const formHookMock = getFormMock(sampleData); beforeEach(() => { diff --git a/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.test.tsx index e531b71e8c90c..12d549a2f71a9 100644 --- a/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.test.tsx @@ -11,9 +11,8 @@ import { EditConnector } from './index'; import { getFormMock, useFormMock } from '../__mock__/form'; import { TestProviders } from '../../../common/mock'; import { connectorsMock } from '../../containers/configure/mock'; -// 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'; -import { act } from 'react-dom/test-utils'; +import { waitFor } from '@testing-library/react'; + jest.mock( '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' ); @@ -67,10 +66,8 @@ describe('EditConnector ', () => { expect(wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().exists()).toBeTruthy(); - await act(async () => { - wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click'); - await waitFor(() => expect(onSubmit.mock.calls[0][0]).toBe(sampleConnector)); - }); + wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click'); + await waitFor(() => expect(onSubmit.mock.calls[0][0]).toBe(sampleConnector)); }); it('Revert to initial external service on error', async () => { @@ -90,12 +87,10 @@ describe('EditConnector ', () => { expect(wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().exists()).toBeTruthy(); - await act(async () => { - wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click'); - await waitFor(() => { - wrapper.update(); - expect(formHookMock.setFieldValue).toHaveBeenCalledWith('connector', 'none'); - }); + wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click'); + await waitFor(() => { + wrapper.update(); + expect(formHookMock.setFieldValue).toHaveBeenCalledWith('connector', 'none'); }); }); @@ -114,15 +109,13 @@ describe('EditConnector ', () => { wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); wrapper.update(); - await act(async () => { - wrapper.find(`[data-test-subj="edit-connectors-cancel"]`).last().simulate('click'); - await waitFor(() => { - wrapper.update(); - expect(formHookMock.setFieldValue).toBeCalledWith( - 'connector', - defaultProps.selectedConnector - ); - }); + wrapper.find(`[data-test-subj="edit-connectors-cancel"]`).last().simulate('click'); + await waitFor(() => { + wrapper.update(); + expect(formHookMock.setFieldValue).toBeCalledWith( + 'connector', + defaultProps.selectedConnector + ); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx index a60167a18762f..013f7bd0a9ba7 100644 --- a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx @@ -6,13 +6,11 @@ import React from 'react'; import { mount } from 'enzyme'; -import { act } from 'react-dom/test-utils'; import { TagList } from '.'; import { getFormMock } from '../__mock__/form'; import { TestProviders } from '../../../common/mock'; -// 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'; +import { waitFor } from '@testing-library/react'; import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; import { useGetTags } from '../../containers/use_get_tags'; @@ -27,6 +25,14 @@ jest.mock( children({ tags: ['rad', 'dude'] }), }) ); +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + return { + ...original, + // eslint-disable-next-line react/display-name + EuiFieldText: () => , + }; +}); const onSubmit = jest.fn(); const defaultProps = { disabled: false, @@ -36,16 +42,6 @@ const defaultProps = { }; describe('TagList ', () => { - // Suppress warnings about "noSuggestions" prop - /* eslint-disable no-console */ - const originalError = console.error; - beforeAll(() => { - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - }); - /* eslint-enable no-console */ const sampleTags = ['coke', 'pepsi']; const fetchTags = jest.fn(); const formHookMock = getFormMock({ tags: sampleTags }); @@ -78,10 +74,8 @@ describe('TagList ', () => { ); wrapper.find(`[data-test-subj="tag-list-edit-button"]`).last().simulate('click'); - await act(async () => { - wrapper.find(`[data-test-subj="edit-tags-submit"]`).last().simulate('click'); - await waitFor(() => expect(onSubmit).toBeCalledWith(sampleTags)); - }); + wrapper.find(`[data-test-subj="edit-tags-submit"]`).last().simulate('click'); + await waitFor(() => expect(onSubmit).toBeCalledWith(sampleTags)); }); it('Tag options render with new tags added', () => { @@ -96,7 +90,7 @@ describe('TagList ', () => { ).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]); }); - it('Cancels on cancel', async () => { + it('Cancels on cancel', () => { const props = { ...defaultProps, tags: ['pepsi'], @@ -109,14 +103,11 @@ describe('TagList ', () => { expect(wrapper.find(`[data-test-subj="tag-pepsi"]`).last().exists()).toBeTruthy(); wrapper.find(`[data-test-subj="tag-list-edit-button"]`).last().simulate('click'); - await act(async () => { - expect(wrapper.find(`[data-test-subj="tag-pepsi"]`).last().exists()).toBeFalsy(); - wrapper.find(`[data-test-subj="edit-tags-cancel"]`).last().simulate('click'); - await waitFor(() => { - wrapper.update(); - expect(wrapper.find(`[data-test-subj="tag-pepsi"]`).last().exists()).toBeTruthy(); - }); - }); + + expect(wrapper.find(`[data-test-subj="tag-pepsi"]`).last().exists()).toBeFalsy(); + wrapper.find(`[data-test-subj="edit-tags-cancel"]`).last().simulate('click'); + wrapper.update(); + expect(wrapper.find(`[data-test-subj="tag-pepsi"]`).last().exists()).toBeTruthy(); }); it('Renders disabled button', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx index 0b376f26a1ae0..4d9b7d030fec0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { mount } from 'enzyme'; import { waitFor } from '@testing-library/react'; -import { act } from 'react-dom/test-utils'; import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; import { getFormMock, useFormMock, useFormDataMock } from '../__mock__/form'; @@ -33,6 +32,7 @@ const defaultProps = { }; const useUpdateCommentMock = useUpdateComment as jest.Mock; jest.mock('../../containers/use_update_comment'); +jest.mock('./user_action_timestamp'); const patchComment = jest.fn(); describe('UserActionTree ', () => { @@ -90,16 +90,14 @@ describe('UserActionTree ', () => { }, caseUserActions: ourActions, }; - - await act(async () => { - const wrapper = mount( - - - - - - ); - + const wrapper = mount( + + + + + + ); + await waitFor(() => { expect(wrapper.find(`[data-test-subj="top-footer"]`).exists()).toBeTruthy(); expect(wrapper.find(`[data-test-subj="bottom-footer"]`).exists()).toBeTruthy(); }); @@ -121,14 +119,14 @@ describe('UserActionTree ', () => { }, }; - await act(async () => { - const wrapper = mount( - - - - - - ); + const wrapper = mount( + + + + + + ); + await waitFor(() => { expect(wrapper.find(`[data-test-subj="top-footer"]`).exists()).toBeTruthy(); expect(wrapper.find(`[data-test-subj="bottom-footer"]`).exists()).toBeFalsy(); }); @@ -141,15 +139,15 @@ describe('UserActionTree ', () => { caseUserActions: ourActions, }; - await act(async () => { - const wrapper = mount( - - - - - - ); + const wrapper = mount( + + + + + + ); + await waitFor(() => { expect( wrapper .find(`[data-test-subj="comment-create-action-${props.data.comments[0].id}"]`) @@ -164,34 +162,32 @@ describe('UserActionTree ', () => { .first() .simulate('click'); - await waitFor(() => { - wrapper.update(); - expect( - wrapper - .find(`[data-test-subj="comment-create-action-${props.data.comments[0].id}"]`) - .first() - .hasClass('outlined') - ).toBeTruthy(); - }); + wrapper.update(); + expect( + wrapper + .find(`[data-test-subj="comment-create-action-${props.data.comments[0].id}"]`) + .first() + .hasClass('outlined') + ).toBeTruthy(); }); }); it('Switches to markdown when edit is clicked and back to panel when canceled', async () => { - await waitFor(() => { - const ourActions = [getUserAction(['comment'], 'create')]; - const props = { - ...defaultProps, - caseUserActions: ourActions, - }; - - const wrapper = mount( - - - - - - ); + const ourActions = [getUserAction(['comment'], 'create')]; + const props = { + ...defaultProps, + caseUserActions: ourActions, + }; + + const wrapper = mount( + + + + + + ); + await waitFor(() => { expect( wrapper .find( @@ -277,24 +273,22 @@ describe('UserActionTree ', () => { .first() .simulate('click'); - await act(async () => { - await waitFor(() => { - wrapper.update(); - expect( - wrapper - .find( - `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` - ) - .exists() - ).toEqual(false); - expect(patchComment).toBeCalledWith({ - commentUpdate: sampleData.content, - caseId: props.data.id, - commentId: props.data.comments[0].id, - fetchUserActions, - updateCase, - version: props.data.comments[0].version, - }); + await waitFor(() => { + wrapper.update(); + expect( + wrapper + .find( + `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(false); + expect(patchComment).toBeCalledWith({ + commentUpdate: sampleData.content, + caseId: props.data.id, + commentId: props.data.comments[0].id, + fetchUserActions, + updateCase, + version: props.data.comments[0].version, }); }); }); @@ -319,89 +313,86 @@ describe('UserActionTree ', () => { .first() .simulate('click'); - await act(async () => { - wrapper - .find(`[data-test-subj="description-action"] [data-test-subj="user-action-save-markdown"]`) - .first() - .simulate('click'); - }); - - wrapper.update(); + wrapper + .find(`[data-test-subj="description-action"] [data-test-subj="user-action-save-markdown"]`) + .first() + .simulate('click'); + await waitFor(() => { + wrapper.update(); - expect( - wrapper - .find(`[data-test-subj="description-action"] [data-test-subj="user-action-markdown-form"]`) - .exists() - ).toEqual(false); + expect( + wrapper + .find( + `[data-test-subj="description-action"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(false); - expect(onUpdateField).toBeCalledWith({ key: 'description', value: sampleData.content }); + expect(onUpdateField).toBeCalledWith({ key: 'description', value: sampleData.content }); + }); }); it('quotes', async () => { - await act(async () => { - const commentData = { - comment: '', - }; - const setFieldValue = jest.fn(); - - const formHookMock = getFormMock(commentData); - useFormMock.mockImplementation(() => ({ form: { ...formHookMock, setFieldValue } })); - - const props = defaultProps; - const wrapper = mount( - - - - - - ); + const commentData = { + comment: '', + }; + const setFieldValue = jest.fn(); - wrapper - .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`) - .first() - .simulate('click'); + const formHookMock = getFormMock(commentData); + useFormMock.mockImplementation(() => ({ form: { ...formHookMock, setFieldValue } })); - await waitFor(() => { - wrapper.update(); + const props = defaultProps; + const wrapper = mount( + + + + + + ); - wrapper - .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-quote"]`) - .first() - .simulate('click'); - }); + wrapper + .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`) + .first() + .simulate('click'); + + await waitFor(() => { + wrapper.update(); - expect(setFieldValue).toBeCalledWith('comment', `> ${props.data.description} \n`); + 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', async () => { const commentId = 'basic-comment-id'; jest.spyOn(routeData, 'useParams').mockReturnValue({ commentId }); - await act(async () => { - const ourActions = [getUserAction(['comment'], 'create')]; - const props = { - ...defaultProps, - caseUserActions: ourActions, - }; - - const wrapper = mount( - - - - - - ); - - await waitFor(() => { - wrapper.update(); - expect( - wrapper - .find(`[data-test-subj="comment-create-action-${commentId}"]`) - .first() - .hasClass('outlined') - ).toBeTruthy(); - }); + const ourActions = [getUserAction(['comment'], 'create')]; + const props = { + ...defaultProps, + caseUserActions: ourActions, + }; + + const wrapper = mount( + + + + + + ); + + await waitFor(() => { + wrapper.update(); + expect( + wrapper + .find(`[data-test-subj="comment-create-action-${commentId}"]`) + .first() + .hasClass('outlined') + ).toBeTruthy(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx index eced73e9c3d67..c8e12adef656a 100644 --- a/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx @@ -6,7 +6,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; - +import { waitFor } from '@testing-library/react'; import { apolloClientObservable, mockGlobalState, @@ -157,39 +157,40 @@ describe('AddFilterToGlobalSearchBar Component', () => { ); + await waitFor(() => { + wrapper.find('[data-test-subj="withHoverActionsButton"]').simulate('mouseenter'); + wrapper.update(); + jest.runAllTimers(); + wrapper.update(); - wrapper.find('[data-test-subj="withHoverActionsButton"]').simulate('mouseenter'); - wrapper.update(); - jest.runAllTimers(); - wrapper.update(); - - wrapper - .find('[data-test-subj="hover-actions-container"] [data-euiicon-type]') - .first() - .simulate('click'); - wrapper.update(); + wrapper + .find('[data-test-subj="hover-actions-container"] [data-euiicon-type]') + .first() + .simulate('click'); + wrapper.update(); - expect(mockAddFilters.mock.calls[0][0]).toEqual({ - meta: { - alias: null, - disabled: false, - key: 'host.name', - negate: false, - params: { - query: 'siem-kibana', - }, - type: 'phrase', - value: 'siem-kibana', - }, - query: { - match: { - 'host.name': { + expect(mockAddFilters.mock.calls[0][0]).toEqual({ + meta: { + alias: null, + disabled: false, + key: 'host.name', + negate: false, + params: { query: 'siem-kibana', - type: 'phrase', }, + type: 'phrase', + value: 'siem-kibana', }, - }, + query: { + match: { + 'host.name': { + query: 'siem-kibana', + type: 'phrase', + }, + }, + }, + }); + expect(onFilterAdded).toHaveBeenCalledTimes(1); }); - expect(onFilterAdded).toHaveBeenCalledTimes(1); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx index eef6e09d496db..e38aaeedad8fd 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx @@ -8,8 +8,7 @@ import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -// 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'; +import { waitFor } from '@testing-library/react'; import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; import { ListSchema } from '../../../lists_plugin_deps'; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts index bbcbcbcf928b3..225b407e4649e 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import moment from 'moment'; import '../../../common/mock/match_media'; import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; @@ -17,6 +17,8 @@ import { import { getOperators, paramIsValid, getGenericComboBoxProps } from './helpers'; describe('helpers', () => { + // @ts-ignore + moment.suppressDeprecationWarnings = true; describe('#getOperators', () => { test('it returns "isOperator" if passed in field is "undefined"', () => { const operator = getOperators(undefined); diff --git a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx index 64c8fde87a6bc..aa638abf65f7e 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx @@ -13,6 +13,7 @@ import { ThemeProvider } from 'styled-components'; import { escapeDataProviderId } from '../drag_and_drop/helpers'; import { TestProviders } from '../../mock'; import '../../mock/match_media'; +import '../../mock/react_beautiful_dnd'; import { BarChartBaseComponent, BarChartComponent } from './barchart'; import { ChartSeriesData } from './common'; @@ -131,19 +132,6 @@ const mockConfig = { customHeight: 324, }; -// Suppress warnings about "react-beautiful-dnd" -/* eslint-disable no-console */ -const originalError = console.error; -const originalWarn = console.warn; -beforeAll(() => { - console.warn = jest.fn(); - console.error = jest.fn(); -}); -afterAll(() => { - console.error = originalError; - console.warn = originalWarn; -}); - describe('BarChartBaseComponent', () => { let shallowWrapper: ShallowWrapper; const mockBarChartData: ChartSeriesData[] = [ @@ -350,7 +338,10 @@ describe.each(chartDataSets)('BarChart with stackByField', () => { )}-${escapeDataProviderId(datum.key)}`; expect( - wrapper.find(`div [data-rbd-draggable-id="${dataProviderId}"]`).first().text() + wrapper + .find(`[draggableId="${dataProviderId}"] [data-test-subj="providerContainer"]`) + .first() + .text() ).toEqual(datum.key); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx index 8fd2fa1fdef12..ffc2404bd4321 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import '../../mock/match_media'; +import '../../mock/react_beautiful_dnd'; import { TestProviders } from '../../mock'; import { MIN_LEGEND_HEIGHT, DraggableLegend } from './draggable_legend'; @@ -58,20 +59,6 @@ const legendItems: LegendItem[] = [ describe('DraggableLegend', () => { const height = 400; - - // Suppress warnings about "react-beautiful-dnd" - /* eslint-disable no-console */ - const originalError = console.error; - const originalWarn = console.warn; - beforeAll(() => { - console.warn = jest.fn(); - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - console.warn = originalWarn; - }); - describe('rendering', () => { let wrapper: ReactWrapper; diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx index 9f6e614c3c285..72e44da3297ea 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import '../../mock/match_media'; +import '../../mock/react_beautiful_dnd'; import { TestProviders } from '../../mock'; import { DraggableLegendItem, LegendItem } from './draggable_legend_item'; @@ -17,19 +18,6 @@ import { DraggableLegendItem, LegendItem } from './draggable_legend_item'; const theme = () => ({ eui: euiDarkVars, darkMode: true }); describe('DraggableLegendItem', () => { - // Suppress warnings about "react-beautiful-dnd" - /* eslint-disable no-console */ - const originalError = console.error; - const originalWarn = console.warn; - beforeAll(() => { - console.warn = jest.fn(); - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - console.warn = originalWarn; - }); - describe('rendering a regular (non "All others") legend item', () => { const legendItem: LegendItem = { color: '#1EA593', diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx index 46e7298677f49..5223452c8b93d 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; import { DraggableStateSnapshot, DraggingStyle } from 'react-beautiful-dnd'; - +import { waitFor } from '@testing-library/react'; import '../../mock/match_media'; import { mockBrowserFields } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; @@ -62,7 +62,7 @@ describe('DraggableWrapper', () => { expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(false); }); - test('it renders hover actions when the mouse is over the text of draggable wrapper', () => { + test('it renders hover actions when the mouse is over the text of draggable wrapper', async () => { const wrapper = mount( @@ -71,11 +71,13 @@ describe('DraggableWrapper', () => { ); - wrapper.find('[data-test-subj="withHoverActionsButton"]').simulate('mouseenter'); - wrapper.update(); - jest.runAllTimers(); - wrapper.update(); - expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true); + await waitFor(() => { + wrapper.find('[data-test-subj="withHoverActionsButton"]').simulate('mouseenter'); + wrapper.update(); + jest.runAllTimers(); + wrapper.update(); + expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx index 8aa926a36988b..af7e9ad5f1492 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -85,22 +85,6 @@ describe('DraggableWrapperHoverContent', () => { }); }); - // Suppress warnings about "react-beautiful-dnd" - /* eslint-disable no-console */ - const originalError = console.error; - const originalWarn = console.warn; - beforeEach(() => { - jest.clearAllMocks(); - }); - beforeAll(() => { - console.warn = jest.fn(); - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - console.warn = originalWarn; - }); - /** * The tests for "Filter for value" and "Filter out value" are similar enough * to combine them into "table tests" using this array diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/link_to_app.test.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/link_to_app.test.tsx index 79773630e0dc0..d791ea44f8198 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/link_to_app.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/link_to_app.test.tsx @@ -44,7 +44,7 @@ describe('LinkToApp component', () => { }); it('should support onClick prop', () => { // Take `_event` (even though it is not used) so that `jest.fn` will have a type that expects to be called with an event - const spyOnClickHandler: LinkToAppOnClickMock = jest.fn((_event) => {}); + const spyOnClickHandler: LinkToAppOnClickMock = jest.fn().mockImplementation((_event) => {}); const renderResult = render( {'link'} @@ -98,20 +98,24 @@ describe('LinkToApp component', () => { }); it('should still preventDefault if onClick callback throws', () => { // Take `_event` (even though it is not used) so that `jest.fn` will have a type that expects to be called with an event - const spyOnClickHandler: LinkToAppOnClickMock = jest.fn((_event) => { + const spyOnClickHandler = jest.fn().mockImplementation((_event) => { throw new Error('test'); }); - const renderResult = render( - - {'link'} - - ); - expect(() => renderResult.find('EuiLink').simulate('click')).toThrow(); - const clickEventArg = spyOnClickHandler.mock.calls[0][0]; - expect(clickEventArg.isDefaultPrevented()).toBe(true); + // eslint-disable-next-line no-empty + try { + } catch (e) { + const renderResult = render( + + {'link'} + + ); + expect(() => renderResult.find('EuiLink').simulate('click')).toThrowError(); + const clickEventArg = spyOnClickHandler.mock.calls[0][0]; + expect(clickEventArg.isDefaultPrevented()).toBe(true); + } }); it('should not navigate if onClick callback prevents default', () => { - const spyOnClickHandler: LinkToAppOnClickMock = jest.fn((ev) => { + const spyOnClickHandler: LinkToAppOnClickMock = jest.fn().mockImplementation((ev) => { ev.preventDefault(); }); const renderResult = render( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx index 01b0810830dd8..c3c7c864ac99b 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx @@ -8,16 +8,19 @@ import { shallow } from 'enzyme'; import React from 'react'; import '../../mock/match_media'; -import { mockDetailItemData, mockDetailItemDataId } from '../../mock/mock_detail_item'; -import { TestProviders } from '../../mock/test_providers'; +import '../../mock/react_beautiful_dnd'; +import { + defaultHeaders, + mockDetailItemData, + mockDetailItemDataId, + TestProviders, +} from '../../mock'; import { EventDetails, View } from './event_details'; import { mockBrowserFields } from '../../containers/source/mock'; -import { defaultHeaders } from '../../mock/header'; import { useMountAppended } from '../../utils/use_mount_appended'; jest.mock('../link_to'); - describe('EventDetails', () => { const mount = useMountAppended(); const onEventToggled = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index 9a3c0fa1cad2e..928521c118ba5 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -8,8 +8,7 @@ import React from 'react'; import useResizeObserver from 'use-resize-observer/polyfilled'; import '../../mock/match_media'; -// 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'; +import { waitFor } from '@testing-library/react'; import { TestProviders } from '../../mock'; import { useMountAppended } from '../../utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index ed1c1c1cdad1f..fa9838aa37015 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -8,10 +8,11 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mount, ReactWrapper } from 'enzyme'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; -import { act } from 'react-dom/test-utils'; +import { waitFor } from '@testing-library/react'; import { AddExceptionModal } from './'; import { useCurrentUser } from '../../../../common/lib/kibana'; +import { useAsync } from '../../../../shared_imports'; import { getExceptionListSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_schema.mock'; import { useFetchIndex } from '../../../containers/source'; import { stubIndexPattern } from 'src/plugins/data/common/index_patterns/index_pattern.stub'; @@ -33,6 +34,7 @@ jest.mock('../../../../detections/containers/detection_engine/rules'); jest.mock('../use_add_exception'); jest.mock('../use_fetch_or_create_rule_exception_list'); jest.mock('../builder'); +jest.mock('../../../../shared_imports'); describe('When the add exception modal is opened', () => { const ruleName = 'test rule'; @@ -48,6 +50,11 @@ describe('When the add exception modal is opened', () => { .spyOn(builder, 'ExceptionBuilderComponent') .mockReturnValue(<>); + (useAsync as jest.Mock).mockImplementation(() => ({ + start: jest.fn(), + loading: false, + })); + (useAddOrUpdateException as jest.Mock).mockImplementation(() => [ { isLoading: false }, jest.fn(), @@ -104,7 +111,7 @@ describe('When the add exception modal is opened', () => { describe('when there is no alert data passed to an endpoint list exception', () => { let wrapper: ReactWrapper; - beforeEach(() => { + beforeEach(async () => { wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { ); const callProps = ExceptionBuilderComponent.mock.calls[0][0]; - act(() => callProps.onChange({ exceptionItems: [] })); + await waitFor(() => callProps.onChange({ exceptionItems: [] })); }); it('has the add exception button disabled', () => { expect( @@ -140,7 +147,7 @@ describe('When the add exception modal is opened', () => { describe('when there is alert data passed to an endpoint list exception', () => { let wrapper: ReactWrapper; - beforeEach(() => { + beforeEach(async () => { const alertDataMock: { ecsData: Ecs; nonEcsData: TimelineNonEcsData[] } = { ecsData: { _id: 'test-id' }, nonEcsData: [{ field: 'file.path', value: ['test/path'] }], @@ -159,7 +166,9 @@ describe('When the add exception modal is opened', () => { ); const callProps = ExceptionBuilderComponent.mock.calls[0][0]; - act(() => callProps.onChange({ exceptionItems: [...callProps.exceptionListItems] })); + await waitFor(() => + callProps.onChange({ exceptionItems: [...callProps.exceptionListItems] }) + ); }); it('has the add exception button enabled', () => { expect( @@ -191,7 +200,7 @@ describe('When the add exception modal is opened', () => { describe('when there is alert data passed to a detection list exception', () => { let wrapper: ReactWrapper; - beforeEach(() => { + beforeEach(async () => { const alertDataMock: { ecsData: Ecs; nonEcsData: TimelineNonEcsData[] } = { ecsData: { _id: 'test-id' }, nonEcsData: [{ field: 'file.path', value: ['test/path'] }], @@ -210,7 +219,9 @@ describe('When the add exception modal is opened', () => { ); const callProps = ExceptionBuilderComponent.mock.calls[0][0]; - act(() => callProps.onChange({ exceptionItems: [getExceptionListItemSchemaMock()] })); + await waitFor(() => + callProps.onChange({ exceptionItems: [getExceptionListItemSchemaMock()] }) + ); }); it('has the add exception button enabled', () => { expect( @@ -243,7 +254,7 @@ describe('When the add exception modal is opened', () => { onChange: (props: { exceptionItems: ExceptionListItemSchema[] }) => void; exceptionListItems: ExceptionListItemSchema[]; }; - beforeEach(() => { + beforeEach(async () => { // Mocks the index patterns to contain the pre-populated endpoint fields so that the exception qualifies as bulk closable (useFetchIndex as jest.Mock).mockImplementation(() => [ false, @@ -278,7 +289,9 @@ describe('When the add exception modal is opened', () => { ); callProps = ExceptionBuilderComponent.mock.calls[0][0]; - act(() => callProps.onChange({ exceptionItems: [...callProps.exceptionListItems] })); + await waitFor(() => + callProps.onChange({ exceptionItems: [...callProps.exceptionListItems] }) + ); }); it('has the add exception button enabled', () => { expect( @@ -307,8 +320,8 @@ describe('When the add exception modal is opened', () => { ).not.toBeDisabled(); }); describe('when a "is in list" entry is added', () => { - it('should have the bulk close checkbox disabled', () => { - act(() => + it('should have the bulk close checkbox disabled', async () => { + await waitFor(() => callProps.onChange({ exceptionItems: [ ...callProps.exceptionListItems, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx index 2d389a7dbcee1..e6f57fe666780 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; -import { wait as waitFor } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; import { fields, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx index d5d2091cc9bc8..551a9173351fc 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx @@ -5,10 +5,10 @@ */ import React from 'react'; +import { waitFor } from '@testing-library/react'; import { ThemeProvider } from 'styled-components'; import { mount, ReactWrapper } from 'enzyme'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; -import { act } from 'react-dom/test-utils'; import { EditExceptionModal } from './'; import { useCurrentUser } from '../../../../common/lib/kibana'; @@ -66,15 +66,14 @@ describe('When the edit exception modal is opened', () => { }); describe('when the modal is loading', () => { - let wrapper: ReactWrapper; - beforeEach(() => { + it('renders the loading spinner', async () => { (useFetchIndex as jest.Mock).mockImplementation(() => [ true, { indexPatterns: stubIndexPattern, }, ]); - wrapper = mount( + const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { /> ); - }); - it('renders the loading spinner', () => { - expect(wrapper.find('[data-test-subj="loadingEditExceptionModal"]').exists()).toBeTruthy(); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="loadingEditExceptionModal"]').exists()).toBeTruthy(); + }); }); }); describe('when an endpoint exception with exception data is passed', () => { describe('when exception entry fields are included in the index pattern', () => { let wrapper: ReactWrapper; - beforeEach(() => { + beforeEach(async () => { const exceptionItemMock = { ...getExceptionListItemSchemaMock(), entries: [ @@ -117,7 +116,9 @@ describe('When the edit exception modal is opened', () => { ); const callProps = ExceptionBuilderComponent.mock.calls[0][0]; - act(() => callProps.onChange({ exceptionItems: [...callProps.exceptionListItems] })); + await waitFor(() => { + callProps.onChange({ exceptionItems: [...callProps.exceptionListItems] }); + }); }); it('has the edit exception button enabled', () => { expect( @@ -145,7 +146,7 @@ describe('When the edit exception modal is opened', () => { describe("when exception entry fields aren't included in the index pattern", () => { let wrapper: ReactWrapper; - beforeEach(() => { + beforeEach(async () => { wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { ); const callProps = ExceptionBuilderComponent.mock.calls[0][0]; - act(() => callProps.onChange({ exceptionItems: [...callProps.exceptionListItems] })); + await waitFor(() => { + callProps.onChange({ exceptionItems: [...callProps.exceptionListItems] }); + }); }); it('has the edit exception button enabled', () => { expect( @@ -189,7 +192,7 @@ describe('When the edit exception modal is opened', () => { describe('when an detection exception with entries is passed', () => { let wrapper: ReactWrapper; - beforeEach(() => { + beforeEach(async () => { wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { ); const callProps = ExceptionBuilderComponent.mock.calls[0][0]; - act(() => callProps.onChange({ exceptionItems: [...callProps.exceptionListItems] })); + await waitFor(() => { + callProps.onChange({ exceptionItems: [...callProps.exceptionListItems] }); + }); }); it('has the edit exception button enabled', () => { expect( @@ -228,7 +233,7 @@ describe('When the edit exception modal is opened', () => { describe('when an exception with no entries is passed', () => { let wrapper: ReactWrapper; - beforeEach(() => { + beforeEach(async () => { const exceptionItemMock = { ...getExceptionListItemSchemaMock(), entries: [] }; wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> @@ -244,7 +249,9 @@ describe('When the edit exception modal is opened', () => { ); const callProps = ExceptionBuilderComponent.mock.calls[0][0]; - act(() => callProps.onChange({ exceptionItems: [...callProps.exceptionListItems] })); + await waitFor(() => { + callProps.onChange({ exceptionItems: [...callProps.exceptionListItems] }); + }); }); it('has the edit exception button disabled', () => { expect( diff --git a/x-pack/plugins/security_solution/public/common/components/last_event_time/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/last_event_time/index.test.tsx index cc0c4d4c837a3..d9b5c5e10893c 100644 --- a/x-pack/plugins/security_solution/public/common/components/last_event_time/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/last_event_time/index.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; - +import '../../../common/mock/formatted_relative'; import { getEmptyValue } from '../empty_value'; import { LastEventIndexKey } from '../../../../common/search_strategy'; import { mockLastEventTimeQuery } from '../../containers/events/last_event_time/mock'; @@ -57,7 +57,7 @@ describe('Last Event Time Stat', () => { ); - expect(wrapper.html()).toBe('Last event: 12 minutes ago'); + expect(wrapper.html()).toBe('Last event: 20 hours ago'); }); test('Bad date time string', async () => { (useTimelineLastEventTime as jest.Mock).mockReturnValue([ diff --git a/x-pack/plugins/security_solution/public/common/components/link_icon/index.tsx b/x-pack/plugins/security_solution/public/common/components/link_icon/index.tsx index 19f1d70e6e230..55842342c6677 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_icon/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_icon/index.tsx @@ -6,16 +6,16 @@ import { EuiIcon, EuiLink, IconSize, IconType } from '@elastic/eui'; import { LinkAnchorProps } from '@elastic/eui/src/components/link/link'; -import React, { ReactNode } from 'react'; +import React, { ReactNode, useCallback, useMemo } from 'react'; import styled, { css } from 'styled-components'; interface LinkProps { + ariaLabel?: string; color?: LinkAnchorProps['color']; disabled?: boolean; href?: string; iconSide?: 'left' | 'right'; onClick?: Function; - ariaLabel?: string; } export const Link = styled(({ iconSide, children, ...rest }) => ( @@ -55,6 +55,7 @@ export interface LinkIconProps extends LinkProps { export const LinkIcon = React.memo( ({ + ariaLabel, children, color, dataTestSubj, @@ -64,21 +65,41 @@ export const LinkIcon = React.memo( iconSize = 's', iconType, onClick, - ariaLabel, - }) => ( - - - {children} - - ) + }) => { + const getChildrenString = useCallback((theChild: string | ReactNode): string => { + if ( + typeof theChild === 'object' && + theChild != null && + 'props' in theChild && + theChild.props && + theChild.props.children + ) { + return getChildrenString(theChild.props.children); + } + return theChild != null && Object.keys(theChild).length > 0 ? (theChild as string) : ''; + }, []); + const aria = useMemo(() => { + if (ariaLabel) { + return ariaLabel; + } + return getChildrenString(children); + }, [ariaLabel, children, getChildrenString]); + + return ( + + + {children} + + ); + } ); LinkIcon.displayName = 'LinkIcon'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx index e9940d088e606..07d148ff96dfa 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx @@ -11,7 +11,6 @@ import '../../mock/match_media'; import { EntityDraggableComponent } from './entity_draggable'; import { TestProviders } from '../../mock/test_providers'; import { useMountAppended } from '../../utils/use_mount_appended'; - describe('entity_draggable', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx index 434cbd8ada88e..cb10c61302d3c 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx @@ -19,7 +19,6 @@ const startDate: string = '2020-07-07T08:20:18.966Z'; const endDate: string = '3000-01-01T00:00:00.000Z'; const narrowDateRange = jest.fn(); - describe('anomaly_scores', () => { let anomalies: Anomalies = cloneDeep(mockAnomalies); const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx index a900c3e49f912..52151f217e01a 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx @@ -19,7 +19,6 @@ import { useMountAppended } from '../../../utils/use_mount_appended'; const startDate: string = '2020-07-07T08:20:18.966Z'; const endDate: string = '3000-01-01T00:00:00.000Z'; const narrowDateRange = jest.fn(); - describe('anomaly_scores', () => { let anomalies: Anomalies = cloneDeep(mockAnomalies); const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx index d370a901a6262..3092cbf265ea9 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx @@ -19,7 +19,6 @@ const startDate = new Date(2001).toISOString(); const endDate = new Date(3000).toISOString(); const interval = 'days'; const narrowDateRange = jest.fn(); - describe('get_anomalies_host_table_columns', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx index 69a4e383413f2..89f94a3819e65 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx @@ -16,7 +16,6 @@ import { useMountAppended } from '../../../utils/use_mount_appended'; const startDate = new Date(2001).toISOString(); const endDate = new Date(3000).toISOString(); - describe('get_anomalies_network_table_columns', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.test.tsx index e58d76bd1dde0..13518acdefdde 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.test.tsx @@ -7,6 +7,7 @@ import { shallow, mount } from 'enzyme'; import React from 'react'; +import { waitFor } from '@testing-library/react'; import { JobSwitchComponent } from './job_switch'; import { cloneDeep } from 'lodash/fp'; import { mockSecurityJobs } from '../api.mock'; @@ -31,7 +32,7 @@ describe('JobSwitch', () => { expect(wrapper).toMatchSnapshot(); }); - test('should call onJobStateChange when the switch is clicked to be true/open', () => { + test('should call onJobStateChange when the switch is clicked to be true/open', async () => { const wrapper = mount( { .simulate('click', { target: { checked: true }, }); - - expect(onJobStateChangeMock.mock.calls[0][0].id).toEqual( - 'linux_anomalous_network_activity_ecs' - ); - expect(onJobStateChangeMock.mock.calls[0][1]).toEqual(1571022859393); - expect(onJobStateChangeMock.mock.calls[0][2]).toEqual(true); + await waitFor(() => { + expect(onJobStateChangeMock.mock.calls[0][0].id).toEqual( + 'linux_anomalous_network_activity_ecs' + ); + expect(onJobStateChangeMock.mock.calls[0][1]).toEqual(1571022859393); + expect(onJobStateChangeMock.mock.calls[0][2]).toEqual(true); + }); }); test('should have a switch when it is not in the loading state', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx index dfcabe7b5aedf..3d7e47a15fc1e 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx @@ -6,6 +6,7 @@ import { shallow, mount } from 'enzyme'; import React from 'react'; +import { waitFor } from '@testing-library/react'; import { JobsTableComponent } from './jobs_table'; import { mockSecurityJobs } from '../api.mock'; import { cloneDeep } from 'lodash/fp'; @@ -59,7 +60,7 @@ describe('JobsTableComponent', () => { ); }); - test('should call onJobStateChange when the switch is clicked to be true/open', () => { + test('should call onJobStateChange when the switch is clicked to be true/open', async () => { const wrapper = mount( { .simulate('click', { target: { checked: true }, }); - expect(onJobStateChangeMock.mock.calls[0]).toEqual([securityJobs[0], 1571022859393, true]); + await waitFor(() => { + expect(onJobStateChangeMock.mock.calls[0]).toEqual([securityJobs[0], 1571022859393, true]); + }); }); test('should have a switch when it is not in the loading state', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx index aa61688f1f986..12199ce5c1b66 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx @@ -6,7 +6,7 @@ import { mount } from 'enzyme'; import React from 'react'; - +import { waitFor } from '@testing-library/react'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { DEFAULT_FROM, DEFAULT_TO } from '../../../../common/constants'; import { TestProviders, mockIndexPattern } from '../../mock'; @@ -16,22 +16,6 @@ import { QueryBar, QueryBarComponentProps } from '.'; const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; describe('QueryBar ', () => { - // We are doing that because we need to wrapped this component with redux - // and redux does not like to be updated and since we need to update our - // child component (BODY) and we do not want to scare anyone with this error - // we are hiding it!!! - // eslint-disable-next-line no-console - const originalError = console.error; - beforeAll(() => { - // eslint-disable-next-line no-console - console.error = (...args: string[]) => { - if (/ does not support changing `store` on the fly/.test(args[0])) { - return; - } - originalError.call(console, ...args); - }; - }); - const mockOnChangeQuery = jest.fn(); const mockOnSubmitQuery = jest.fn(); const mockOnSavedQuery = jest.fn(); @@ -372,7 +356,7 @@ describe('QueryBar ', () => { }); describe('SavedQueryManagementComponent state', () => { - test('popover should hidden when "Save current query" button was clicked', () => { + test('popover should hidden when "Save current query" button was clicked', async () => { const Proxy = (props: QueryBarComponentProps) => ( @@ -397,21 +381,24 @@ describe('QueryBar ', () => { onSavedQuery={mockOnSavedQuery} /> ); + await waitFor(() => { + const isSavedQueryPopoverOpen = () => + wrapper.find('EuiPopover[id="savedQueryPopover"]').prop('isOpen'); - const isSavedQueryPopoverOpen = () => - wrapper.find('EuiPopover[id="savedQueryPopover"]').prop('isOpen'); - - expect(isSavedQueryPopoverOpen()).toBeFalsy(); + expect(isSavedQueryPopoverOpen()).toBeFalsy(); - wrapper - .find('button[data-test-subj="saved-query-management-popover-button"]') - .simulate('click'); + wrapper + .find('button[data-test-subj="saved-query-management-popover-button"]') + .simulate('click'); - expect(isSavedQueryPopoverOpen()).toBeTruthy(); + expect(isSavedQueryPopoverOpen()).toBeTruthy(); - wrapper.find('button[data-test-subj="saved-query-management-save-button"]').simulate('click'); + wrapper + .find('button[data-test-subj="saved-query-management-save-button"]') + .simulate('click'); - expect(isSavedQueryPopoverOpen()).toBeFalsy(); + expect(isSavedQueryPopoverOpen()).toBeFalsy(); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx index bd9f2677ec966..2696b115cdc18 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx @@ -20,8 +20,7 @@ import { } from '../../mock'; import { createStore, State } from '../../store'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { act } from 'react-dom/test-utils'; -import { wait as waitFor } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; const mockDispatch = jest.fn(); jest.mock('react-redux', () => { @@ -126,13 +125,11 @@ describe('Sourcerer component', () => { expect(true).toBeTruthy(); wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); - await act(async () => { + await waitFor(() => { ((wrapper.find(EuiComboBox).props() as unknown) as { onChange: (a: EuiComboBoxOptionOption[]) => void; }).onChange([mockOptions[0], mockOptions[1]]); - await waitFor(() => { - wrapper.update(); - }); + wrapper.update(); }); wrapper.find(`[data-test-subj="add-index"]`).first().simulate('click'); diff --git a/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx index b28c7e70b8ae8..f091f22abcb94 100644 --- a/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx @@ -17,7 +17,6 @@ import { import { TestProviders } from '../../mock'; import { getEmptyValue } from '../empty_value'; import { useMountAppended } from '../../utils/use_mount_appended'; - describe('Table Helpers', () => { const items = ['item1', 'item2', 'item3']; const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx index 594bffbd4ff63..fd1fa1c29a807 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx @@ -6,7 +6,7 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; - +import { waitFor } from '@testing-library/react'; import '../../mock/match_media'; import { mockBrowserFields } from '../../containers/source/mock'; import { @@ -180,19 +180,6 @@ let testProps = { }; describe('StatefulTopN', () => { - // Suppress warnings about "react-beautiful-dnd" - /* eslint-disable no-console */ - const originalError = console.error; - const originalWarn = console.warn; - beforeAll(() => { - console.warn = jest.fn(); - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - console.warn = originalWarn; - }); - describe('rendering in a global NON-timeline context', () => { let wrapper: ReactWrapper; @@ -343,7 +330,7 @@ describe('StatefulTopN', () => { }); }); describe('rendering in a NON-active timeline context', () => { - test(`defaults to the 'Alert events' option when rendering in a NON-active timeline context (e.g. the Alerts table on the Detections page) when 'documentType' from 'useTimelineTypeContext()' is 'alerts'`, () => { + test(`defaults to the 'Alert events' option when rendering in a NON-active timeline context (e.g. the Alerts table on the Detections page) when 'documentType' from 'useTimelineTypeContext()' is 'alerts'`, async () => { const filterManager = new FilterManager(mockUiSettingsForFilterManager); const manageTimelineForTesting = { @@ -365,10 +352,11 @@ describe('StatefulTopN', () => { ); + await waitFor(() => { + const props = wrapper.find('[data-test-subj="top-n"]').first().props() as Props; - const props = wrapper.find('[data-test-subj="top-n"]').first().props() as Props; - - expect(props.defaultView).toEqual('alert'); + expect(props.defaultView).toEqual('alert'); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx index 829f918ddfe1b..f7ad35f2c5a37 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx @@ -6,15 +6,13 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; - +import { waitFor } from '@testing-library/react'; import '../../mock/match_media'; import { TestProviders, mockIndexPattern } from '../../mock'; import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; import { allEvents, defaultOptions } from './helpers'; -import { TopN } from './top_n'; -import { TimelineEventsType } from '../../../../common/types/timeline'; -import { InputsModelId } from '../../store/inputs/constants'; +import { TopN, Props as TopNProps } from './top_n'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -90,28 +88,15 @@ const combinedQueries = { }; describe('TopN', () => { - // Suppress warnings about "react-beautiful-dnd" - /* eslint-disable no-console */ - const originalError = console.error; - const originalWarn = console.warn; - beforeAll(() => { - console.warn = jest.fn(); - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - console.warn = originalWarn; - }); - const query = { query: '', language: 'kuery' }; const toggleTopN = jest.fn(); - const eventTypes: { [id: string]: TimelineEventsType } = { + const eventTypes: { [id: string]: TopNProps['defaultView'] } = { raw: 'raw', alert: 'alert', all: 'all', }; - let testProps = { + let testProps: TopNProps = { defaultView: eventTypes.raw, field, filters: [], @@ -121,7 +106,7 @@ describe('TopN', () => { options: defaultOptions, query, setAbsoluteRangeDatePicker, - setAbsoluteRangeDatePickerTarget: 'global' as InputsModelId, + setAbsoluteRangeDatePickerTarget: 'global', setQuery: jest.fn(), to: '2020-04-15T00:31:47.695Z', toggleTopN, @@ -172,28 +157,35 @@ describe('TopN', () => { }); describe('alerts view', () => { - let wrapper: ReactWrapper; - - beforeEach(() => { + beforeAll(() => { testProps = { ...testProps, defaultView: eventTypes.alert, }; - wrapper = mount( + }); + + test(`it renders SignalsByCategory when defaultView is 'alert'`, async () => { + const wrapper = mount( ); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="alerts-histogram-panel"]').exists()).toBe(true); + }); }); - test(`it renders SignalsByCategory when defaultView is 'alert'`, () => { - expect(wrapper.find('[data-test-subj="alerts-histogram-panel"]').exists()).toBe(true); - }); - - test(`it does NOT render EventsByDataset when defaultView is 'alert'`, () => { - expect( - wrapper.find('[data-test-subj="eventsByDatasetOverview-uuid.v4()Panel"]').exists() - ).toBe(false); + test(`it does NOT render EventsByDataset when defaultView is 'alert'`, async () => { + const wrapper = mount( + + + + ); + await waitFor(() => { + expect( + wrapper.find('[data-test-subj="eventsByDatasetOverview-uuid.v4()Panel"]').exists() + ).toBe(false); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx index fc970c066e8a5..f4a48eaea69c2 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx @@ -21,8 +21,7 @@ import { } from './test_dependencies'; import { UrlStateContainerPropTypes } from './types'; import { useUrlStateHooks } from './use_url_state'; -// 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'; +import { waitFor } from '@testing-library/react'; let mockProps: UrlStateContainerPropTypes; diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/upgrade.ts b/x-pack/plugins/security_solution/public/common/hooks/endpoint/upgrade.ts new file mode 100644 index 0000000000000..48f826d1c3a91 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/endpoint/upgrade.ts @@ -0,0 +1,95 @@ +/* + * 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 { useEffect } from 'react'; +import { HttpFetchOptions, HttpStart } from 'src/core/public'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { + epmRouteService, + appRoutesService, + CheckPermissionsResponse, + BulkInstallPackagesResponse, +} from '../../../../../ingest_manager/common'; +import { StartServices } from '../../../types'; +import { useIngestEnabledCheck } from './ingest_enabled'; + +/** + * Requests that the endpoint package be upgraded to the latest version + * + * @param http an http client for sending the request + * @param options an object containing options for the request + */ +const sendUpgradeEndpointPackage = async ( + http: HttpStart, + options: HttpFetchOptions = {} +): Promise => { + return http.post(epmRouteService.getBulkInstallPath(), { + ...options, + body: JSON.stringify({ + packages: ['endpoint'], + }), + }); +}; + +/** + * Checks with the ingest manager if the current user making these requests has the right permissions + * to install the endpoint package. + * + * @param http an http client for sending the request + * @param options an object containing options for the request + */ +const sendCheckPermissions = async ( + http: HttpStart, + options: HttpFetchOptions = {} +): Promise => { + return http.get(appRoutesService.getCheckPermissionsPath(), { + ...options, + }); +}; + +export const useUpgradeEndpointPackage = () => { + const context = useKibana(); + const { allEnabled: ingestEnabled } = useIngestEnabledCheck(); + + useEffect(() => { + const abortController = new AbortController(); + + // cancel any ongoing requests + const abortRequests = () => { + abortController.abort(); + }; + + if (ingestEnabled) { + const signal = abortController.signal; + + (async () => { + try { + // make sure we're a privileged user before trying to install the package + const { success: hasPermissions } = await sendCheckPermissions(context.services.http, { + signal, + }); + + // if we're not a privileged user then return and don't try to check the status of the endpoint package + if (!hasPermissions) { + return abortRequests; + } + + // ignore the response for now since we aren't notifying the user + await sendUpgradeEndpointPackage(context.services.http, { signal }); + } catch (error) { + // Ignore Errors, since this should not hinder the user's ability to use the UI + + // ignore the error that occurs from aborting a request + if (!abortController.signal.aborted) { + // eslint-disable-next-line no-console + console.error(error); + } + } + + return abortRequests; + })(); + } + }, [ingestEnabled, context.services.http]); +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts index c026b65853a4c..6f8ff2e1bb21a 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts @@ -13,20 +13,20 @@ import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_r import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { securityMock } from '../../../../../../plugins/security/public/mocks'; import { - DEFAULT_APP_TIME_RANGE, DEFAULT_APP_REFRESH_INTERVAL, - DEFAULT_INDEX_KEY, + DEFAULT_APP_TIME_RANGE, + DEFAULT_BYTES_FORMAT, + DEFAULT_DARK_MODE, DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ, - DEFAULT_DARK_MODE, - DEFAULT_TIME_RANGE, - DEFAULT_REFRESH_RATE_INTERVAL, DEFAULT_FROM, - DEFAULT_TO, + DEFAULT_INDEX_KEY, + DEFAULT_INDEX_PATTERN, DEFAULT_INTERVAL_PAUSE, DEFAULT_INTERVAL_VALUE, - DEFAULT_BYTES_FORMAT, - DEFAULT_INDEX_PATTERN, + DEFAULT_REFRESH_RATE_INTERVAL, + DEFAULT_TIME_RANGE, + DEFAULT_TO, } from '../../../../common/constants'; import { StartServices } from '../../../types'; import { createSecuritySolutionStorageMock } from '../../mock/mock_local_storage'; @@ -77,14 +77,43 @@ export const createStartServicesMock = (): StartServices => { const data = dataPluginMock.createStartContract(); const security = securityMock.createSetup(); - const services = ({ + return ({ ...core, - data, + data: { + ...data, + query: { + ...data.query, + savedQueries: { + ...data.query.savedQueries, + getAllSavedQueries: jest.fn(() => + Promise.resolve({ + id: '123', + attributes: { + total: 123, + }, + }) + ), + findSavedQueries: jest.fn(() => + Promise.resolve({ + total: 123, + queries: [], + }) + ), + }, + }, + search: { + ...data.search, + search: jest.fn().mockImplementation(() => ({ + subscribe: jest.fn().mockImplementation(() => ({ + error: jest.fn(), + next: jest.fn(), + })), + })), + }, + }, security, storage, } as unknown) as StartServices; - - return services; }; export const createWithKibanaMock = () => { diff --git a/x-pack/plugins/security_solution/public/common/mock/formatted_relative.ts b/x-pack/plugins/security_solution/public/common/mock/formatted_relative.ts new file mode 100644 index 0000000000000..0eb1c9a478ca0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/mock/formatted_relative.ts @@ -0,0 +1,15 @@ +/* + * 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. + */ + +jest.mock('@kbn/i18n/react', () => { + const originalModule = jest.requireActual('@kbn/i18n/react'); + const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); + + return { + ...originalModule, + FormattedRelative, + }; +}); diff --git a/x-pack/plugins/security_solution/public/common/mock/react_beautiful_dnd.ts b/x-pack/plugins/security_solution/public/common/mock/react_beautiful_dnd.ts new file mode 100644 index 0000000000000..e077d28925912 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/mock/react_beautiful_dnd.ts @@ -0,0 +1,53 @@ +/* + * 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 { + DraggableProvided, + DraggableStateSnapshot, + DroppableProvided, + DroppableStateSnapshot, +} from 'react-beautiful-dnd'; +import React from 'react'; + +jest.mock('react-beautiful-dnd', () => ({ + Droppable: ({ + children, + }: { + children: (a: DroppableProvided, b: DroppableStateSnapshot) => void; + }) => + children( + { + droppableProps: { + 'data-rbd-droppable-context-id': '123', + 'data-rbd-droppable-id': '123', + }, + innerRef: jest.fn(), + }, + { + isDraggingOver: false, + isUsingPlaceholder: false, + } + ), + Draggable: ({ + children, + }: { + children: (a: DraggableProvided, b: DraggableStateSnapshot) => void; + }) => + children( + { + draggableProps: { + 'data-rbd-draggable-context-id': '123', + 'data-rbd-draggable-id': '123', + }, + innerRef: jest.fn(), + }, + { + isDragging: false, + isDropAnimating: false, + } + ), + DragDropContext: ({ children }: { children: React.ReactNode }) => children, +})); diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index 8c186addf783d..e84f80655fbe4 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -46,7 +46,7 @@ export const kibanaObservable = new BehaviorSubject(createStartServicesMock()); Object.defineProperty(window, 'localStorage', { value: localStorageMock(), }); - +window.scrollTo = jest.fn(); const MockKibanaContextProvider = createKibanaContextProviderMock(); const { storage } = createSecuritySolutionStorageMock(); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index f326d5ad54ef2..47da1e93cf004 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -44,6 +44,7 @@ describe('alert actions', () => { updateTimelineIsLoading = jest.fn() as jest.Mocked; searchStrategyClient = { aggs: {} as ISearchStart['aggs'], + showError: jest.fn(), search: jest.fn().mockResolvedValue({ data: mockTimelineDetails }), searchSource: {} as ISearchStart['searchSource'], }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx index 0ddf4d06fb0fc..366bd156e0c1b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx @@ -69,7 +69,9 @@ const RuleStatusComponent: React.FC = ({ ruleId, ruleEnabled }) <> - {currentStatus?.status ?? getEmptyTagValue()} + + {currentStatus?.status ?? getEmptyTagValue()} + {currentStatus?.status_date != null && currentStatus?.status != null && ( @@ -84,6 +86,7 @@ const RuleStatusComponent: React.FC = ({ ruleId, ruleEnabled }) )} } > diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx index 4312be0b46990..2bdc813639740 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { ThemeProvider } from 'styled-components'; import euiDarkVars from '@elastic/eui/dist/eui_theme_light.json'; +import { act } from '@testing-library/react'; import { stubIndexPattern } from 'src/plugins/data/common/index_patterns/index_pattern.stub'; import { StepAboutRule } from '.'; @@ -23,18 +24,17 @@ import { fillEmptySeverityMappings } from '../../../pages/detection_engine/rules jest.mock('../../../../common/containers/source'); const theme = () => ({ eui: euiDarkVars, darkMode: true }); - -/* eslint-disable no-console */ -// Silence until enzyme fixed to use ReactTestUtils.act() -const originalError = console.error; -beforeAll(() => { - console.error = jest.fn(); -}); -afterAll(() => { - console.error = originalError; +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + return { + ...original, + // eslint-disable-next-line react/display-name, @typescript-eslint/no-explicit-any + EuiFieldText: (props: any) => { + const { isInvalid, isLoading, fullWidth, inputRef, isDisabled, ...validInputProps } = props; + return ; + }, + }; }); -/* eslint-enable no-console */ - describe('StepAboutRuleComponent', () => { let formHook: RuleStepsFormHooks[RuleStep.aboutRule] | null = null; const setFormHook = ( @@ -54,7 +54,7 @@ describe('StepAboutRuleComponent', () => { ]); }); - test('it renders StepRuleDescription if isReadOnlyView is true and "name" property exists', () => { + it('it renders StepRuleDescription if isReadOnlyView is true and "name" property exists', () => { const wrapper = shallow( { ); - if (!formHook) { - throw new Error('Form hook not set, but tests depend on it'); - } - - wrapper - .find('[data-test-subj="detectionEngineStepAboutRuleName"] input') - .first() - .simulate('change', { target: { value: 'Test name text' } }); - - const result = await formHook(); - expect(result?.isValid).toEqual(false); + await act(async () => { + if (!formHook) { + throw new Error('Form hook not set, but tests depend on it'); + } + wrapper + .find('[data-test-subj="detectionEngineStepAboutRuleName"] input') + .first() + .simulate('change', { target: { value: 'Test name text' } }); + + const result = await formHook(); + expect(result?.isValid).toEqual(false); + }); }); it('is invalid if no "name" is present', async () => { @@ -109,17 +110,18 @@ describe('StepAboutRuleComponent', () => { ); - if (!formHook) { - throw new Error('Form hook not set, but tests depend on it'); - } - - wrapper - .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] textarea') - .first() - .simulate('change', { target: { value: 'Test description text' } }); - - const result = await formHook(); - expect(result?.isValid).toEqual(false); + await act(async () => { + if (!formHook) { + throw new Error('Form hook not set, but tests depend on it'); + } + + wrapper + .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] textarea') + .first() + .simulate('change', { target: { value: 'Test description text' } }); + const result = await formHook(); + expect(result?.isValid).toEqual(false); + }); }); it('is valid if both "name" and "description" are present', async () => { @@ -136,10 +138,6 @@ describe('StepAboutRuleComponent', () => { ); - if (!formHook) { - throw new Error('Form hook not set, but tests depend on it'); - } - wrapper .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] textarea') .first() @@ -173,12 +171,17 @@ describe('StepAboutRuleComponent', () => { ], }; - const result = await formHook(); - expect(result?.isValid).toEqual(true); - expect(result?.data).toEqual(expected); + await act(async () => { + if (!formHook) { + throw new Error('Form hook not set, but tests depend on it'); + } + const result = await formHook(); + expect(result?.isValid).toEqual(true); + expect(result?.data).toEqual(expected); + }); }); - test('it allows user to set the risk score as a number (and not a string)', async () => { + it('it allows user to set the risk score as a number (and not a string)', async () => { const wrapper = mount( { ); - if (!formHook) { - throw new Error('Form hook not set, but tests depend on it'); - } - wrapper .find('[data-test-subj="detectionEngineStepAboutRuleName"] input') .first() @@ -235,9 +234,14 @@ describe('StepAboutRuleComponent', () => { ], }; - const result = await formHook(); - expect(result?.isValid).toEqual(true); - expect(result?.data).toEqual(expected); + await act(async () => { + if (!formHook) { + throw new Error('Form hook not set, but tests depend on it'); + } + const result = await formHook(); + expect(result?.isValid).toEqual(true); + expect(result?.data).toEqual(expected); + }); }); it('does not modify the provided risk score until the user changes the severity', async () => { @@ -254,10 +258,6 @@ describe('StepAboutRuleComponent', () => { ); - if (!formHook) { - throw new Error('Form hook not set, but tests depend on it'); - } - wrapper .find('[data-test-subj="detectionEngineStepAboutRuleName"] input') .first() @@ -268,18 +268,23 @@ describe('StepAboutRuleComponent', () => { .first() .simulate('change', { target: { value: 'Test description text' } }); - const result = await formHook(); - expect(result?.isValid).toEqual(true); - expect(result?.data?.riskScore.value).toEqual(21); - - wrapper - .find('[data-test-subj="detectionEngineStepAboutRuleSeverity"] [data-test-subj="select"]') - .last() - .simulate('click'); - wrapper.find('button#medium').simulate('click'); - - const result2 = await formHook(); - expect(result2?.isValid).toEqual(true); - expect(result2?.data?.riskScore.value).toEqual(47); + await act(async () => { + if (!formHook) { + throw new Error('Form hook not set, but tests depend on it'); + } + const result = await formHook(); + expect(result?.isValid).toEqual(true); + expect(result?.data?.riskScore.value).toEqual(21); + + wrapper + .find('[data-test-subj="detectionEngineStepAboutRuleSeverity"] [data-test-subj="select"]') + .last() + .simulate('click'); + wrapper.find('button#medium').simulate('click'); + + const result2 = await formHook(); + expect(result2?.isValid).toEqual(true); + expect(result2?.data?.riskScore.value).toEqual(47); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.test.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.test.tsx index 591e1c81cd2ad..bae5a237bd124 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.test.tsx @@ -5,7 +5,7 @@ */ import React, { FormEvent } from 'react'; import { mount, ReactWrapper } from 'enzyme'; -import { act } from 'react-dom/test-utils'; +import { waitFor } from '@testing-library/react'; import { TestProviders } from '../../../common/mock'; import { ValueListsForm } from './form'; @@ -24,7 +24,7 @@ const mockSelectFile:

(container: ReactWrapper

, file: File) => Promise { const fileChange = container.find('EuiFilePicker').prop('onChange'); - act(() => { + await waitFor(() => { if (fileChange) { fileChange(({ item: () => file } as unknown) as FormEvent); } diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.test.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.test.tsx index ff743d1d5090a..dc40c997bc840 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { act } from 'react-dom/test-utils'; +import { waitFor } from '@testing-library/react'; import { getListResponseMock } from '../../../../../lists/common/schemas/response/list_schema.mock'; import { exportList, useDeleteList, useFindLists, ListSchema } from '../../../shared_imports'; @@ -46,7 +46,6 @@ describe('ValueListsModal', () => { ); expect(container.find('EuiModal')).toHaveLength(0); - container.unmount(); }); it('renders modal if showModal is true', () => { @@ -57,7 +56,6 @@ describe('ValueListsModal', () => { ); expect(container.find('EuiModal')).toHaveLength(1); - container.unmount(); }); it('calls onClose when modal is closed', () => { @@ -71,7 +69,6 @@ describe('ValueListsModal', () => { container.find('button[data-test-subj="value-lists-modal-close-action"]').simulate('click'); expect(onClose).toHaveBeenCalled(); - container.unmount(); }); it('renders ValueListsForm and an EuiTable', () => { @@ -83,29 +80,27 @@ describe('ValueListsModal', () => { expect(container.find('ValueListsForm')).toHaveLength(1); expect(container.find('EuiBasicTable')).toHaveLength(1); - container.unmount(); }); describe('modal table actions', () => { - it('calls exportList when export is clicked', () => { + it('calls exportList when export is clicked', async () => { const container = mount( ); - act(() => { + await waitFor(() => { container .find('button[data-test-subj="action-export-value-list"]') .first() .simulate('click'); - container.unmount(); }); expect(exportList).toHaveBeenCalledWith(expect.objectContaining({ listId: 'some-list-id' })); }); - it('calls deleteList when delete is clicked', () => { + it('calls deleteList when delete is clicked', async () => { const deleteListMock = jest.fn(); (useDeleteList as jest.Mock).mockReturnValue({ start: deleteListMock, @@ -117,12 +112,11 @@ describe('ValueListsModal', () => { ); - act(() => { + await waitFor(() => { container .find('button[data-test-subj="action-delete-value-list"]') .first() .simulate('click'); - container.unmount(); }); expect(deleteListMock).toHaveBeenCalledWith(expect.objectContaining({ id: 'some-list-id' })); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index a5d21d2847586..0982b5740b893 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { useParams } from 'react-router-dom'; - +import { waitFor } from '@testing-library/react'; import '../../../common/mock/match_media'; import { apolloClientObservable, @@ -80,7 +80,7 @@ describe('DetectionEnginePageComponent', () => { }); }); - it('renders correctly', () => { + it('renders correctly', async () => { const wrapper = mount( @@ -93,7 +93,8 @@ describe('DetectionEnginePageComponent', () => { ); - - expect(wrapper.find('FiltersGlobal').exists()).toBe(true); + await waitFor(() => { + expect(wrapper.find('FiltersGlobal').exists()).toBe(true); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx index 9f486dc11e99d..13c6985a30c2b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx @@ -6,12 +6,11 @@ import React from 'react'; import { shallow, mount } from 'enzyme'; -import { act } from 'react-dom/test-utils'; import '../../../../../common/mock/match_media'; +import '../../../../../common/mock/formatted_relative'; import { TestProviders } from '../../../../../common/mock'; -// 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'; +import { waitFor } from '@testing-library/react'; import { AllRules } from './index'; jest.mock('react-router-dom', () => { @@ -198,11 +197,9 @@ describe('AllRules', () => { ); - await act(async () => { - await waitFor(() => { - expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeFalsy(); - expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeTruthy(); - }); + await waitFor(() => { + expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeFalsy(); + expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeTruthy(); }); }); @@ -226,12 +223,10 @@ describe('AllRules', () => { const monitoringTab = wrapper.find('[data-test-subj="allRulesTableTab-monitoring"] button'); monitoringTab.simulate('click'); - await act(async () => { - await waitFor(() => { - wrapper.update(); - expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeTruthy(); - expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeFalsy(); - }); + await waitFor(() => { + wrapper.update(); + expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeTruthy(); + expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeFalsy(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx index 22c3c43fb2356..afa4777e74856 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { mount } from 'enzyme'; +import { waitFor } from '@testing-library/react'; import '../../../../../common/mock/match_media'; import { @@ -77,7 +78,7 @@ describe('RuleDetailsPageComponent', () => { }); }); - it('renders correctly', () => { + it('renders correctly', async () => { const wrapper = mount( @@ -93,7 +94,8 @@ describe('RuleDetailsPageComponent', () => { wrappingComponent: TestProviders, } ); - - expect(wrapper.find('[data-test-subj="header-page-title"]').exists()).toBe(true); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="header-page-title"]').exists()).toBe(true); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx index f11b0ac4ec3f8..8545e5da512bb 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import moment from 'moment'; import { GetStepsData, getDefineStepsData, @@ -31,6 +31,8 @@ import { } from './types'; describe('rule helpers', () => { + // @ts-ignore + moment.suppressDeprecationWarnings = true; describe('getStepsData', () => { test('returns object with about, define, schedule and actions step properties formatted', () => { const { diff --git a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx index 4f64cca45d162..4f2dff47af5a6 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx @@ -6,8 +6,7 @@ import React from 'react'; -// we don't have the types for waitFor just yet, so using "as waitFor" until when we do -import { render, act, wait as waitFor } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { useFirstLastSeenHost } from '../../containers/hosts/first_last_seen'; import { TestProviders } from '../../../common/mock'; @@ -26,16 +25,6 @@ describe('FirstLastSeen Component', () => { const firstSeen = 'Apr 8, 2019 @ 16:09:40.692'; const lastSeen = 'Apr 8, 2019 @ 18:35:45.064'; - // Suppress warnings about "react-apollo" until we migrate to apollo@3 - /* eslint-disable no-console */ - const originalError = console.error; - beforeAll(() => { - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - }); - test('Loading', async () => { useFirstLastSeenHostMock.mockReturnValue([true, MOCKED_RESPONSE]); const { container } = render( @@ -66,13 +55,11 @@ describe('FirstLastSeen Component', () => { ); - await act(() => - waitFor(() => { - expect(container.innerHTML).toBe( - `

${firstSeen}
` - ); - }) - ); + await waitFor(() => { + expect(container.innerHTML).toBe( + `
${firstSeen}
` + ); + }); }); test('Last Seen', async () => { @@ -87,13 +74,11 @@ describe('FirstLastSeen Component', () => { /> ); - await act(() => - waitFor(() => { - expect(container.innerHTML).toBe( - `
${lastSeen}
` - ); - }) - ); + await waitFor(() => { + expect(container.innerHTML).toBe( + `
${lastSeen}
` + ); + }); }); test('First Seen is empty but not Last Seen', async () => { @@ -115,13 +100,11 @@ describe('FirstLastSeen Component', () => { ); - await act(() => - waitFor(() => { - expect(container.innerHTML).toBe( - `
${lastSeen}
` - ); - }) - ); + await waitFor(() => { + expect(container.innerHTML).toBe( + `
${lastSeen}
` + ); + }); }); test('Last Seen is empty but not First Seen', async () => { @@ -143,13 +126,11 @@ describe('FirstLastSeen Component', () => { ); - await act(() => - waitFor(() => { - expect(container.innerHTML).toBe( - `
${firstSeen}
` - ); - }) - ); + await waitFor(() => { + expect(container.innerHTML).toBe( + `
${firstSeen}
` + ); + }); }); test('First Seen With a bad date time string', async () => { @@ -170,11 +151,9 @@ describe('FirstLastSeen Component', () => { /> ); - await act(() => - waitFor(() => { - expect(container.textContent).toBe('something-invalid'); - }) - ); + await waitFor(() => { + expect(container.textContent).toBe('something-invalid'); + }); }); test('Last Seen With a bad date time string', async () => { @@ -195,10 +174,8 @@ describe('FirstLastSeen Component', () => { /> ); - await act(() => - waitFor(() => { - expect(container.textContent).toBe('something-invalid'); - }) - ); + await waitFor(() => { + expect(container.textContent).toBe('something-invalid'); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/endpoint_pagination.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/endpoint_pagination.test.ts index b4e00319485e9..1e3a92e6ec135 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/endpoint_pagination.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/endpoint_pagination.test.ts @@ -26,6 +26,10 @@ import { } from '../../../../common/store/test_utils'; import { getEndpointListPath } from '../../../common/routing'; +jest.mock('../../policy/store/policy_list/services/ingest', () => ({ + sendGetAgentPolicyList: () => Promise.resolve({ items: [] }), + sendGetEndpointSecurityPackage: () => Promise.resolve({}), +})); describe('endpoint list pagination: ', () => { let fakeCoreStart: jest.Mocked; let depsStart: DepsStartMock; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index c4d2886f3e8e5..d19b3a0ce4177 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -24,8 +24,9 @@ import { endpointMiddlewareFactory } from './middleware'; import { getEndpointListPath } from '../../../common/routing'; jest.mock('../../policy/store/policy_list/services/ingest', () => ({ - sendGetEndpointSecurityPackage: () => Promise.resolve({}), sendGetAgentConfigList: () => Promise.resolve({ items: [] }), + sendGetAgentPolicyList: () => Promise.resolve({ items: [] }), + sendGetEndpointSecurityPackage: () => Promise.resolve({}), })); describe('endpoint list middleware', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 51a6be18471aa..bb4be42b04d4e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -7,7 +7,8 @@ import React from 'react'; import * as reactTestingLibrary from '@testing-library/react'; import { EndpointList } from './index'; -import '../../../../common/mock/match_media.ts'; +import '../../../../common/mock/match_media'; + import { mockEndpointDetailsApiResult, mockEndpointResultList, @@ -26,8 +27,25 @@ import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_da import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants'; import { mockPolicyResultList } from '../../policy/store/policy_list/test_mock_utils'; -jest.mock('../../../../common/components/link_to'); +// not sure why this can't be imported from '../../../../common/mock/formatted_relative'; +// but sure enough it needs to be inline in this one file +jest.mock('@kbn/i18n/react', () => { + const originalModule = jest.requireActual('@kbn/i18n/react'); + const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); + return { + ...originalModule, + FormattedRelative, + }; +}); +jest.mock('../../../../common/components/link_to'); +jest.mock('../../policy/store/policy_list/services/ingest', () => { + const originalModule = jest.requireActual('../../policy/store/policy_list/services/ingest'); + return { + ...originalModule, + sendGetEndpointSecurityPackage: () => Promise.resolve({}), + }; +}); describe('when on the list page', () => { const docGenerator = new EndpointDocGenerator(); let render: () => ReturnType; @@ -35,7 +53,6 @@ describe('when on the list page', () => { let store: AppContextTestRender['store']; let coreStart: AppContextTestRender['coreStart']; let middlewareSpy: AppContextTestRender['middlewareSpy']; - beforeEach(() => { const mockedContext = createAppRootMockRenderer(); ({ history, store, coreStart, middlewareSpy } = mockedContext); diff --git a/x-pack/plugins/security_solution/public/management/pages/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/index.test.tsx index c04d3b1ec1a90..bb947310644f8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.test.tsx @@ -15,10 +15,21 @@ jest.mock('../../common/hooks/endpoint/ingest_enabled'); describe('when in the Admistration tab', () => { let render: () => ReturnType; + let coreStart: AppContextTestRender['coreStart']; beforeEach(() => { const mockedContext = createAppRootMockRenderer(); + coreStart = mockedContext.coreStart; render = () => mockedContext.render(); + coreStart.http.get.mockImplementation(() => + Promise.resolve({ + response: [ + { + name: 'endpoint', + }, + ], + }) + ); }); it('should display the No Permissions view when Ingest is OFF', async () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx index 959753cba7bd7..2d29e33c8d3d5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx @@ -71,14 +71,12 @@ const NoPermissions = memo(() => { /> } body={ -

- - - -

+ + + } /> diff --git a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx index 3539fe717e14d..f600c15f4c7d8 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx @@ -7,7 +7,7 @@ import { mount } from 'enzyme'; import React from 'react'; import { Router } from 'react-router-dom'; - +import { waitFor } from '@testing-library/react'; import '../../common/mock/match_media'; import { Filter } from '../../../../../../src/plugins/data/common/es_query'; import { useSourcererScope } from '../../common/containers/sourcerer'; @@ -71,7 +71,7 @@ const mockProps = { hasMlUserPermissions: true, }; const mockUseSourcererScope = useSourcererScope as jest.Mock; -describe('rendering - rendering', () => { +describe('Network page - rendering', () => { test('it renders the Setup Instructions text when no index is available', () => { mockUseSourcererScope.mockReturnValue({ selectedPatterns: [], @@ -88,7 +88,7 @@ describe('rendering - rendering', () => { expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); }); - test('it DOES NOT render the Setup Instructions text when an index is available', () => { + test('it DOES NOT render the Setup Instructions text when an index is available', async () => { mockUseSourcererScope.mockReturnValue({ selectedPatterns: [], indicesExist: true, @@ -101,10 +101,12 @@ describe('rendering - rendering', () => { ); - expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(false); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(false); + }); }); - test('it should add the new filters after init', () => { + test('it should add the new filters after init', async () => { const newFilters: Filter[] = [ { query: { @@ -157,12 +159,14 @@ describe('rendering - rendering', () => { ); - wrapper.update(); + await waitFor(() => { + wrapper.update(); - myStore.dispatch(inputsActions.setSearchBarFilter({ id: 'global', filters: newFilters })); - wrapper.update(); - expect(wrapper.find(NetworkRoutes).props().filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"match_all":{}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"ItRocks"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}' - ); + myStore.dispatch(inputsActions.setSearchBarFilter({ id: 'global', filters: newFilters })); + wrapper.update(); + expect(wrapper.find(NetworkRoutes).props().filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"match_all":{}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"ItRocks"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}' + ); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx index 6f1b7e95e763d..704506d9813d9 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx @@ -10,9 +10,9 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import '../../../common/mock/match_media'; +import '../../../common/mock/react_beautiful_dnd'; import { useMatrixHistogram } from '../../../common/containers/matrix_histogram'; -// 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'; +import { waitFor } from '@testing-library/react'; import { mockIndexPattern, TestProviders } from '../../../common/mock'; import { AlertsByCategory } from '.'; diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx index fee38ad3c6289..bb47fcd5512fc 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx @@ -8,6 +8,7 @@ import { mount } from 'enzyme'; import React from 'react'; import '../../../../common/mock/match_media'; +import '../../../../common/mock/react_beautiful_dnd'; import { TestProviders } from '../../../../common/mock'; import { EndpointOverview } from './index'; diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx index 5406b444cee56..3d275a961bb2a 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx @@ -5,8 +5,8 @@ */ import React, { FunctionComponent } from 'react'; -import { render, act, RenderResult, fireEvent } from '@testing-library/react'; -import { renderHook, act as hooksAct } from '@testing-library/react-hooks'; +import { render, waitFor, RenderResult, fireEvent } from '@testing-library/react'; +import { renderHook, act } from '@testing-library/react-hooks'; import { useCamera, useAutoUpdatingClientRect } from './use_camera'; import { Provider } from 'react-redux'; import * as selectors from '../store/selectors'; @@ -69,8 +69,8 @@ describe('useCamera on an unpainted element', () => { const topMargin = 20; const centerX = width / 2 + leftMargin; const centerY = height / 2 + topMargin; - beforeEach(() => { - act(() => { + beforeEach(async () => { + await waitFor(() => { simulator.controls.simulateElementResize(element, { width, height, @@ -97,11 +97,11 @@ describe('useCamera on an unpainted element', () => { const resizeObserverSpy = jest.spyOn(simulator.mock.ResizeObserver.prototype, 'observe'); let [rect, ref] = result.current; - hooksAct(() => ref(element)); + act(() => ref(element)); expect(resizeObserverSpy).toHaveBeenCalledWith(element); const div = document.createElement('div'); - hooksAct(() => ref(div)); + act(() => ref(div)); expect(resizeObserverSpy).toHaveBeenCalledWith(div); [rect, ref] = result.current; @@ -161,7 +161,7 @@ describe('useCamera on an unpainted element', () => { }); describe('when the camera begins animation', () => { let process: SafeResolverEvent; - beforeEach(() => { + beforeEach(async () => { const events: SafeResolverEvent[] = []; const numberOfEvents: number = 10; @@ -184,7 +184,7 @@ describe('useCamera on an unpainted element', () => { type: 'serverReturnedResolverData', payload: { result: tree, parameters: mockTreeFetcherParameters() }, }; - act(() => { + await waitFor(() => { store.dispatch(serverResponseAction); }); } else { @@ -205,7 +205,7 @@ describe('useCamera on an unpainted element', () => { process, }, }; - act(() => { + await waitFor(() => { store.dispatch(cameraAction); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.test.tsx index 62306046c7b8c..4f5e3c814751c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.test.tsx @@ -16,7 +16,6 @@ import { TestProviders } from '../../../common/mock'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; import * as i18n from './translations'; - describe('Category', () => { const timelineId = 'test'; const selectedCategoryId = 'client'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx index 90f4444562c5a..a3a19d3877fa1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx @@ -6,7 +6,7 @@ import { mount } from 'enzyme'; import React from 'react'; - +import { waitFor } from '@testing-library/react'; import { mockBrowserFields } from '../../../common/containers/source/mock'; import { TestProviders } from '../../../common/mock'; import '../../../common/mock/match_media'; @@ -44,17 +44,19 @@ describe('FieldName', () => { ).toEqual(timestampFieldId); }); - test('it renders a copy to clipboard action menu item a user hovers over the name', () => { + test('it renders a copy to clipboard action menu item a user hovers over the name', async () => { const wrapper = mount( ); - wrapper.find('[data-test-subj="withHoverActionsButton"]').at(0).simulate('mouseenter'); - wrapper.update(); - jest.runAllTimers(); - wrapper.update(); - expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true); + await waitFor(() => { + wrapper.find('[data-test-subj="withHoverActionsButton"]').at(0).simulate('mouseenter'); + wrapper.update(); + jest.runAllTimers(); + wrapper.update(); + expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true); + }); }); test('it highlights the text specified by the `highlight` prop', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx index a3c7440bece24..3bfeabc614ea9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx @@ -6,8 +6,10 @@ import { mount } from 'enzyme'; import React from 'react'; +import { waitFor } from '@testing-library/react'; import '../../../common/mock/match_media'; +import '../../../common/mock/react_beautiful_dnd'; import { mockBrowserFields } from '../../../common/containers/source/mock'; import { TestProviders } from '../../../common/mock'; @@ -15,19 +17,6 @@ import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers'; import { StatefulFieldsBrowserComponent } from '.'; -// Suppress warnings about "react-beautiful-dnd" until we migrate to @testing-library/react -/* eslint-disable no-console */ -const originalError = console.error; -const originalWarn = console.warn; -beforeAll(() => { - console.warn = jest.fn(); - console.error = jest.fn(); -}); -afterAll(() => { - console.error = originalError; - console.warn = originalWarn; -}); - describe('StatefulFieldsBrowser', () => { const timelineId = 'test'; @@ -93,7 +82,7 @@ describe('StatefulFieldsBrowser', () => { beforeEach(() => { jest.useFakeTimers(); }); - test('it updates the selectedCategoryId state, which makes the category bold, when the user clicks a category name in the left hand side of the field browser', () => { + test('it updates the selectedCategoryId state, which makes the category bold, when the user clicks a category name in the left hand side of the field browser', async () => { const wrapper = mount( { wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); wrapper.find(`.field-browser-category-pane-auditd-${timelineId}`).first().simulate('click'); - - wrapper.update(); - expect( - wrapper.find(`.field-browser-category-pane-auditd-${timelineId}`).at(1) - ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); + await waitFor(() => { + wrapper.update(); + expect( + wrapper.find(`.field-browser-category-pane-auditd-${timelineId}`).at(1) + ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); + }); }); - test('it updates the selectedCategoryId state according to most fields returned', () => { + test('it updates the selectedCategoryId state according to most fields returned', async () => { const wrapper = mount( { ); - wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); - expect( - wrapper.find(`.field-browser-category-pane-cloud-${timelineId}`).at(1) - ).toHaveStyleRule('font-weight', 'normal', { modifier: '.euiText' }); - wrapper - .find('[data-test-subj="field-search"]') - .last() - .simulate('change', { target: { value: 'cloud' } }); - - jest.runOnlyPendingTimers(); - wrapper.update(); - expect( - wrapper.find(`.field-browser-category-pane-cloud-${timelineId}`).at(1) - ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); + await waitFor(() => { + wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); + expect( + wrapper.find(`.field-browser-category-pane-cloud-${timelineId}`).at(1) + ).toHaveStyleRule('font-weight', 'normal', { modifier: '.euiText' }); + wrapper + .find('[data-test-subj="field-search"]') + .last() + .simulate('change', { target: { value: 'cloud' } }); + + jest.runOnlyPendingTimers(); + wrapper.update(); + expect( + wrapper.find(`.field-browser-category-pane-cloud-${timelineId}`).at(1) + ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx index aa19fb6f68ed4..95ad5285507c5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx @@ -8,6 +8,7 @@ import { mount, shallow } from 'enzyme'; import { set } from '@elastic/safer-lodash-set/fp'; import React from 'react'; import { ActionCreator } from 'typescript-fsa'; +import '../../../common/mock/react_beautiful_dnd'; import { apolloClientObservable, diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/index.test.tsx index a927627353f69..8aebc8519bcb7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/index.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ThemeProvider } from 'styled-components'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import '../../../../common/mock/formatted_relative'; import { NoteCard } from '.'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_card_header.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_card_header.test.tsx index fcb0d0294fa22..bc46c238c5ae8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_card_header.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_card_header.test.tsx @@ -7,6 +7,7 @@ import moment from 'moment-timezone'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import '../../../../common/mock/formatted_relative'; import * as i18n from '../translations'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_created.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_created.test.tsx index 4bb9a6b666a87..7b51a9eaa1a2d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_created.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_created.test.tsx @@ -7,6 +7,7 @@ import moment from 'moment-timezone'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import '../../../../common/mock/formatted_relative'; import { NoteCreated } from './note_created'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx index 952295d0858ee..5506514999f35 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ThemeProvider } from 'styled-components'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import '../../../../common/mock/formatted_relative'; import { Note } from '../../../../common/lib/note'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx index 64b9db59467e1..f6ac1ab4cec3e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx @@ -14,11 +14,11 @@ import { waitFor } from '@testing-library/react'; import { useHistory, useParams } from 'react-router-dom'; import '../../../common/mock/match_media'; +import '../../../common/mock/formatted_relative'; import { SecurityPageName } from '../../../app/types'; import { TimelineType } from '../../../../common/types/timeline'; -import { TestProviders, apolloClient } from '../../../common/mock/test_providers'; -import { mockOpenTimelineQueryResults } from '../../../common/mock/timeline_results'; +import { TestProviders, apolloClient, mockOpenTimelineQueryResults } from '../../../common/mock'; import { getTimelineTabsUrl } from '../../../common/components/link_to'; import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines_page'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx index 3f737a5ba73db..cb0eba910c342 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx @@ -10,6 +10,7 @@ import moment from 'moment'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ThemeProvider } from 'styled-components'; +import '../../../../common/mock/formatted_relative'; import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { OpenTimelineResult, TimelineResultNote } from '../types'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/note_preview.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/note_preview.test.tsx index e23c9b7fe2083..feabe46edfa82 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/note_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/note_preview.test.tsx @@ -8,6 +8,7 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ThemeProvider } from 'styled-components'; +import '../../../../common/mock/formatted_relative'; import { getEmptyValue } from '../../../../common/components/empty_value'; import { NotePreview } from './note_preview'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx index 3d5c5f60d1d9b..2d5849463270b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx @@ -9,7 +9,7 @@ import { cloneDeep } from 'lodash/fp'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ThemeProvider } from 'styled-components'; -import { act } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; import '../../../common/mock/match_media'; import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines_page'; @@ -293,7 +293,7 @@ describe('OpenTimeline', () => { ); wrapper.find('[data-test-subj="utility-bar-action"]').find('EuiLink').simulate('click'); - await act(async () => { + await waitFor(() => { expect( wrapper.find('[data-test-subj="export-timeline-action"]').first().prop('disabled') ).toEqual(true); @@ -313,7 +313,7 @@ describe('OpenTimeline', () => { ); wrapper.find('[data-test-subj="utility-bar-action"]').find('EuiLink').simulate('click'); - await act(async () => { + await waitFor(() => { expect( wrapper.find('[data-test-subj="delete-timeline-action"]').first().prop('disabled') ).toEqual(true); @@ -333,7 +333,7 @@ describe('OpenTimeline', () => { ); wrapper.find('[data-test-subj="utility-bar-action"]').find('EuiLink').simulate('click'); - await act(async () => { + await waitFor(() => { expect( wrapper.find('[data-test-subj="export-timeline-action"]').first().prop('disabled') ).toEqual(false); @@ -353,7 +353,7 @@ describe('OpenTimeline', () => { ); wrapper.find('[data-test-subj="utility-bar-action"]').find('EuiLink').simulate('click'); - await act(async () => { + await waitFor(() => { expect( wrapper.find('[data-test-subj="delete-timeline-action"]').first().prop('disabled') ).toEqual(false); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx index 9632b0e6ecea4..2744e0b42efce 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx @@ -49,7 +49,7 @@ describe('OpenTimelineModal', () => { sortField: DEFAULT_SORT_FIELD, timelineType: TimelineType.default, timelineStatus: TimelineStatus.active, - templateTimelineFilter: [
], + templateTimelineFilter: [
], title, totalSearchResultsCount: mockSearchResults.length, }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx index ea587aeca2061..35b6c99c04176 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx @@ -10,8 +10,7 @@ import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { ThemeProvider } from 'styled-components'; -// 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'; +import { waitFor } from '@testing-library/react'; import { TestProviders } from '../../../../common/mock/test_providers'; import { mockOpenTimelineQueryResults } from '../../../../common/mock/timeline_results'; import * as i18n from '../translations'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx index b8b2630e09c6e..18270a30eb0e1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx @@ -12,6 +12,7 @@ import { ThemeProvider } from 'styled-components'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import '../../../../common/mock/match_media'; +import '../../../../common/mock/formatted_relative'; import { getEmptyValue } from '../../../../common/components/empty_value'; import { OpenTimelineResult } from '../types'; import { mockTimelineResults } from '../../../../common/mock/timeline_results'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx index efad85775a9e4..a08820e9435d3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx @@ -20,13 +20,14 @@ const IconType = styled(EuiIcon)` `; IconType.displayName = 'IconType'; -const P = styled.p` +const P = styled.span` margin-bottom: 5px; `; P.displayName = 'P'; const ToolTipTableMetadata = styled.span` margin-right: 5px; + display: block; `; ToolTipTableMetadata.displayName = 'ToolTipTableMetadata'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 8b859c1746a46..8fa5d18c0c4f5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -15,8 +15,7 @@ import { TestProviders } from '../../../../common/mock/test_providers'; import { Body, BodyProps } from '.'; import { columnRenderers, rowRenderers } from './renderers'; import { Sort } from './sort'; -// 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'; +import { waitFor } from '@testing-library/react'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { SELECTOR_TIMELINE_BODY_CLASS_NAME, TimelineBody } from '../styles'; import { TimelineType } from '../../../../../common/types/timeline'; @@ -195,22 +194,6 @@ describe('Body', () => { wrapper.update(); }; - // We are doing that because we need to wrapped this component with redux - // and redux does not like to be updated and since we need to update our - // child component (BODY) and we do not want to scare anyone with this error - // we are hiding it!!! - // eslint-disable-next-line no-console - const originalError = console.error; - beforeAll(() => { - // eslint-disable-next-line no-console - console.error = (...args: string[]) => { - if (/ does not support changing `store` on the fly/.test(args[0])) { - return; - } - originalError.call(console, ...args); - }; - }); - beforeEach(() => { dispatchAddNoteToEvent.mockClear(); dispatchOnPinEvent.mockClear(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx index efb19275336db..19344a7fd7c9b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx @@ -22,8 +22,7 @@ import { useThrottledResizeObserver } from '../../../../common/components/utils' import { Properties, showDescriptionThreshold, showNotesThreshold } from '.'; import { setInsertTimeline } from '../../../store/timeline/actions'; export { nextTick } from '../../../../../../../test_utils'; - -import { act } from 'react-dom/test-utils'; +import { waitFor } from '@testing-library/react'; jest.mock('../../../../common/components/link_to'); @@ -372,18 +371,16 @@ describe('Properties', () => { wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click'); wrapper.find('[data-test-subj="attach-timeline-case"]').first().simulate('click'); - await act(async () => { - await Promise.resolve({}); + await waitFor(() => { + expect(mockNavigateToApp).toBeCalledWith('securitySolution:case', { path: '/create' }); + expect(mockDispatch).toBeCalledWith( + setInsertTimeline({ + timelineId: defaultProps.timelineId, + timelineSavedObjectId: '1', + timelineTitle: 'coolness', + }) + ); }); - - expect(mockNavigateToApp).toBeCalledWith('securitySolution:case', { path: '/create' }); - expect(mockDispatch).toBeCalledWith( - setInsertTimeline({ - timelineId: defaultProps.timelineId, - timelineSavedObjectId: '1', - timelineTitle: 'coolness', - }) - ); }); test('insert timeline - existing case', async () => { @@ -397,9 +394,8 @@ describe('Properties', () => { wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click'); wrapper.find('[data-test-subj="attach-timeline-existing-case"]').first().simulate('click'); - await act(async () => { - await Promise.resolve({}); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="all-cases-modal"]').exists()).toBeTruthy(); }); - expect(wrapper.find('[data-test-subj="all-cases-modal"]').exists()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx index 7da3cf940da50..c21592bed12e0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { act as actDom } from 'react-dom/test-utils'; +import { waitFor } from '@testing-library/react'; import { renderHook, act } from '@testing-library/react-hooks'; import { mount, shallow } from 'enzyme'; @@ -86,7 +86,7 @@ describe('useCreateTimelineButton', () => { await waitForNextUpdate(); const button = result.current.getButton({ outline: false, title: 'mock title' }); - actDom(() => { + await waitFor(() => { const wrapper = mount(button); wrapper.update(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx index 6c8fd4975c657..87956647c11f1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx @@ -24,22 +24,6 @@ const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; jest.mock('../../../../common/lib/kibana'); describe('Timeline QueryBar ', () => { - // We are doing that because we need to wrapped this component with redux - // and redux does not like to be updated and since we need to update our - // child component (BODY) and we do not want to scare anyone with this error - // we are hiding it!!! - // eslint-disable-next-line no-console - const originalError = console.error; - beforeAll(() => { - // eslint-disable-next-line no-console - console.error = (...args: string[]) => { - if (/ does not support changing `store` on the fly/.test(args[0])) { - return; - } - originalError.call(console, ...args); - }; - }); - const mockApplyKqlFilterQuery = jest.fn(); const mockSetFilters = jest.fn(); const mockSetKqlFilterQueryDraft = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.test.tsx index b64ca0ccc0b35..519372d0ac797 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.test.tsx @@ -7,31 +7,18 @@ import React from 'react'; import { shallow, ShallowWrapper, mount } from 'enzyme'; import { TimelineType } from '../../../../../common/types/timeline'; import { SortFieldTimeline, Direction } from '../../../../graphql/types'; -import { SearchProps } from './'; +import { SelectableTimeline, ORIGINAL_PAGE_SIZE, SearchProps } from './'; +const mockFetchAllTimeline = jest.fn(); +jest.mock('../../../containers/all', () => { + return { + useGetAllTimeline: jest.fn(() => ({ + fetchAllTimeline: mockFetchAllTimeline, + timelines: [], + })), + }; +}); describe('SelectableTimeline', () => { - const mockFetchAllTimeline = jest.fn(); - const mockEuiSelectable = jest.fn(); - - jest.doMock('@elastic/eui', () => { - const originalModule = jest.requireActual('@elastic/eui'); - return { - ...originalModule, - EuiSelectable: mockEuiSelectable.mockImplementation(({ children }) =>
{children}
), - }; - }); - - jest.doMock('../../../containers/all', () => { - return { - useGetAllTimeline: jest.fn(() => ({ - fetchAllTimeline: mockFetchAllTimeline, - timelines: [], - })), - }; - }); - - const { SelectableTimeline, ORIGINAL_PAGE_SIZE } = jest.requireActual('./'); - const props = { hideUntitled: false, getSelectableOptions: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts index 8ef740f7bc1d7..a439699d27f6d 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts @@ -11,157 +11,101 @@ import { ImportDataProps } from '../../detections/containers/detection_engine/ru jest.mock('../../common/lib/kibana', () => { return { - KibanaServices: { get: jest.fn(() => ({ http: { fetch: jest.fn() } })) }, + KibanaServices: { + get: jest.fn(() => ({ + http: { + fetch: jest.fn(), + }, + })), + }, }; }); +const timelineData = { + columns: [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + }, + { + columnHeaderType: 'not-filtered', + id: 'event.action', + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + }, + ], + dataProviders: [], + description: 'x', + eventType: 'all', + filters: [], + kqlMode: 'filter', + kqlQuery: { + filterQuery: null, + }, + title: '', + timelineType: TimelineType.default, + templateTimelineVersion: null, + templateTimelineId: null, + dateRange: { + start: 1590998565409, + end: 1591084965409, + }, + savedQueryId: null, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + status: TimelineStatus.active, +}; +const mockPatchTimelineResponse = { + data: { + persistTimeline: { + code: 200, + message: 'success', + timeline: { + ...timelineData, + savedObjectId: '9d5693e0-a42a-11ea-b8f4-c5434162742a', + version: 'WzM0NSwxXQ==', + }, + }, + }, +}; describe('persistTimeline', () => { describe('create draft timeline', () => { const timelineId = null; const initialDraftTimeline = { - columns: [ - { - columnHeaderType: 'not-filtered', - id: '@timestamp', - }, - { - columnHeaderType: 'not-filtered', - id: 'message', - }, - { - columnHeaderType: 'not-filtered', - id: 'event.category', - }, - { - columnHeaderType: 'not-filtered', - id: 'event.action', - }, - { - columnHeaderType: 'not-filtered', - id: 'host.name', - }, - { - columnHeaderType: 'not-filtered', - id: 'source.ip', - }, - { - columnHeaderType: 'not-filtered', - id: 'destination.ip', - }, - { - columnHeaderType: 'not-filtered', - id: 'user.name', - }, - ], - dataProviders: [], - description: 'x', - eventType: 'all', - filters: [], - kqlMode: 'filter', - kqlQuery: { - filterQuery: null, - }, - title: '', - timelineType: TimelineType.default, - templateTimelineVersion: null, - templateTimelineId: null, - dateRange: { - start: 1590998565409, - end: 1591084965409, - }, - savedQueryId: null, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + ...timelineData, status: TimelineStatus.draft, }; const mockDraftResponse = { data: { persistTimeline: { timeline: { + ...initialDraftTimeline, savedObjectId: '9d5693e0-a42a-11ea-b8f4-c5434162742a', version: 'WzMzMiwxXQ==', - columns: [ - { columnHeaderType: 'not-filtered', id: '@timestamp' }, - { columnHeaderType: 'not-filtered', id: 'message' }, - { columnHeaderType: 'not-filtered', id: 'event.category' }, - { columnHeaderType: 'not-filtered', id: 'event.action' }, - { columnHeaderType: 'not-filtered', id: 'host.name' }, - { columnHeaderType: 'not-filtered', id: 'source.ip' }, - { columnHeaderType: 'not-filtered', id: 'destination.ip' }, - { columnHeaderType: 'not-filtered', id: 'user.name' }, - ], - dataProviders: [], - description: '', - eventType: 'all', - filters: [], - kqlMode: 'filter', - timelineType: 'default', - kqlQuery: { filterQuery: null }, - title: '', - sort: { columnId: '@timestamp', sortDirection: 'desc' }, - status: 'draft', - created: 1591091394733, - createdBy: 'angela', - updated: 1591091394733, - updatedBy: 'angela', - templateTimelineId: null, - templateTimelineVersion: null, - dateRange: { start: 1590998565409, end: 1591084965409 }, - savedQueryId: null, - favorite: [], - eventIdToNoteIds: [], - noteIds: [], - notes: [], - pinnedEventIds: [], - pinnedEventsSaveObject: [], - }, - }, - }, - }; - const mockPatchTimelineResponse = { - data: { - persistTimeline: { - code: 200, - message: 'success', - timeline: { - savedObjectId: '9d5693e0-a42a-11ea-b8f4-c5434162742a', - version: 'WzM0NSwxXQ==', - columns: [ - { columnHeaderType: 'not-filtered', id: '@timestamp' }, - { columnHeaderType: 'not-filtered', id: 'message' }, - { columnHeaderType: 'not-filtered', id: 'event.category' }, - { columnHeaderType: 'not-filtered', id: 'event.action' }, - { columnHeaderType: 'not-filtered', id: 'host.name' }, - { columnHeaderType: 'not-filtered', id: 'source.ip' }, - { columnHeaderType: 'not-filtered', id: 'destination.ip' }, - { columnHeaderType: 'not-filtered', id: 'user.name' }, - ], - dataProviders: [], - description: 'x', - eventType: 'all', - filters: [], - kqlMode: 'filter', - timelineType: 'default', - kqlQuery: { filterQuery: null }, - title: '', - sort: { columnId: '@timestamp', sortDirection: 'desc' }, - status: 'draft', - created: 1591092702804, - createdBy: 'angela', - updated: 1591092705206, - updatedBy: 'angela', - templateTimelineId: null, - templateTimelineVersion: null, - dateRange: { start: 1590998565409, end: 1591084965409 }, - savedQueryId: null, - favorite: [], - eventIdToNoteIds: [], - noteIds: [], - notes: [], - pinnedEventIds: [], - pinnedEventsSaveObject: [], }, }, }, @@ -223,61 +167,7 @@ describe('persistTimeline', () => { describe('create draft timeline in read-only permission', () => { const timelineId = null; const initialDraftTimeline = { - columns: [ - { - columnHeaderType: 'not-filtered', - id: '@timestamp', - }, - { - columnHeaderType: 'not-filtered', - id: 'message', - }, - { - columnHeaderType: 'not-filtered', - id: 'event.category', - }, - { - columnHeaderType: 'not-filtered', - id: 'event.action', - }, - { - columnHeaderType: 'not-filtered', - id: 'host.name', - }, - { - columnHeaderType: 'not-filtered', - id: 'source.ip', - }, - { - columnHeaderType: 'not-filtered', - id: 'destination.ip', - }, - { - columnHeaderType: 'not-filtered', - id: 'user.name', - }, - ], - dataProviders: [], - description: 'x', - eventType: 'all', - filters: [], - kqlMode: 'filter', - kqlQuery: { - filterQuery: null, - }, - title: '', - timelineType: TimelineType.default, - templateTimelineVersion: null, - templateTimelineId: null, - dateRange: { - start: 1590998565409, - end: 1591084965409, - }, - savedQueryId: null, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + ...timelineData, status: TimelineStatus.draft, }; @@ -325,104 +215,14 @@ describe('persistTimeline', () => { describe('create active timeline (import)', () => { const timelineId = null; - const importTimeline = { - columns: [ - { - columnHeaderType: 'not-filtered', - id: '@timestamp', - }, - { - columnHeaderType: 'not-filtered', - id: 'message', - }, - { - columnHeaderType: 'not-filtered', - id: 'event.category', - }, - { - columnHeaderType: 'not-filtered', - id: 'event.action', - }, - { - columnHeaderType: 'not-filtered', - id: 'host.name', - }, - { - columnHeaderType: 'not-filtered', - id: 'source.ip', - }, - { - columnHeaderType: 'not-filtered', - id: 'destination.ip', - }, - { - columnHeaderType: 'not-filtered', - id: 'user.name', - }, - ], - dataProviders: [], - description: 'x', - eventType: 'all', - filters: [], - kqlMode: 'filter', - kqlQuery: { - filterQuery: null, - }, - title: '', - timelineType: TimelineType.default, - templateTimelineVersion: null, - templateTimelineId: null, - dateRange: { - start: 1590998565409, - end: 1591084965409, - }, - savedQueryId: null, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, - status: TimelineStatus.active, - }; + const importTimeline = timelineData; const mockPostTimelineResponse = { data: { persistTimeline: { timeline: { + ...timelineData, savedObjectId: '9d5693e0-a42a-11ea-b8f4-c5434162742a', version: 'WzMzMiwxXQ==', - columns: [ - { columnHeaderType: 'not-filtered', id: '@timestamp' }, - { columnHeaderType: 'not-filtered', id: 'message' }, - { columnHeaderType: 'not-filtered', id: 'event.category' }, - { columnHeaderType: 'not-filtered', id: 'event.action' }, - { columnHeaderType: 'not-filtered', id: 'host.name' }, - { columnHeaderType: 'not-filtered', id: 'source.ip' }, - { columnHeaderType: 'not-filtered', id: 'destination.ip' }, - { columnHeaderType: 'not-filtered', id: 'user.name' }, - ], - dataProviders: [], - description: '', - eventType: 'all', - filters: [], - kqlMode: 'filter', - timelineType: 'default', - kqlQuery: { filterQuery: null }, - title: '', - sort: { columnId: '@timestamp', sortDirection: 'desc' }, - status: 'draft', - created: 1591091394733, - createdBy: 'angela', - updated: 1591091394733, - updatedBy: 'angela', - templateTimelineId: null, - templateTimelineVersion: null, - dateRange: { start: 1590998565409, end: 1591084965409 }, - savedQueryId: null, - favorite: [], - eventIdToNoteIds: [], - noteIds: [], - notes: [], - pinnedEventIds: [], - pinnedEventsSaveObject: [], }, }, }, @@ -462,104 +262,16 @@ describe('persistTimeline', () => { describe('update active timeline', () => { const timelineId = '9d5693e0-a42a-11ea-b8f4-c5434162742a'; - const inputTimeline = { - columns: [ - { - columnHeaderType: 'not-filtered', - id: '@timestamp', - }, - { - columnHeaderType: 'not-filtered', - id: 'message', - }, - { - columnHeaderType: 'not-filtered', - id: 'event.category', - }, - { - columnHeaderType: 'not-filtered', - id: 'event.action', - }, - { - columnHeaderType: 'not-filtered', - id: 'host.name', - }, - { - columnHeaderType: 'not-filtered', - id: 'source.ip', - }, - { - columnHeaderType: 'not-filtered', - id: 'destination.ip', - }, - { - columnHeaderType: 'not-filtered', - id: 'user.name', - }, - ], - dataProviders: [], - description: 'x', - eventType: 'all', - filters: [], - kqlMode: 'filter', - kqlQuery: { - filterQuery: null, - }, - title: '', - timelineType: TimelineType.default, - templateTimelineVersion: null, - templateTimelineId: null, - dateRange: { - start: 1590998565409, - end: 1591084965409, - }, - savedQueryId: null, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, - status: TimelineStatus.active, - }; - const mockPatchTimelineResponse = { + const inputTimeline = timelineData; + const mockPatchTimelineResponseNew = { data: { persistTimeline: { timeline: { - savedObjectId: '9d5693e0-a42a-11ea-b8f4-c5434162742a', + ...mockPatchTimelineResponse.data.persistTimeline.timeline, version: 'WzMzMiwxXQ==', - columns: [ - { columnHeaderType: 'not-filtered', id: '@timestamp' }, - { columnHeaderType: 'not-filtered', id: 'message' }, - { columnHeaderType: 'not-filtered', id: 'event.category' }, - { columnHeaderType: 'not-filtered', id: 'event.action' }, - { columnHeaderType: 'not-filtered', id: 'host.name' }, - { columnHeaderType: 'not-filtered', id: 'source.ip' }, - { columnHeaderType: 'not-filtered', id: 'destination.ip' }, - { columnHeaderType: 'not-filtered', id: 'user.name' }, - ], - dataProviders: [], - description: '', - eventType: 'all', - filters: [], - kqlMode: 'filter', - timelineType: 'default', - kqlQuery: { filterQuery: null }, - title: '', - sort: { columnId: '@timestamp', sortDirection: 'desc' }, - status: 'draft', - created: 1591091394733, - createdBy: 'angela', - updated: 1591091394733, - updatedBy: 'angela', - templateTimelineId: null, - templateTimelineVersion: null, - dateRange: { start: 1590998565409, end: 1591084965409 }, - savedQueryId: null, - favorite: [], - eventIdToNoteIds: [], - noteIds: [], - notes: [], - pinnedEventIds: [], - pinnedEventsSaveObject: [], + description: 'x', + created: 1591092702804, + updated: 1591092705206, }, }, }, @@ -578,7 +290,7 @@ describe('persistTimeline', () => { http: { fetch: fetchMock, post: postMock, - patch: patchMock.mockReturnValue(mockPatchTimelineResponse), + patch: patchMock.mockReturnValue(mockPatchTimelineResponseNew), }, }); api.persistTimeline({ timelineId, timeline: inputTimeline, version }); @@ -674,7 +386,7 @@ describe('getDraftTimeline', () => { (KibanaServices.get as jest.Mock).mockReturnValue({ http: { - get: getMock, + get: getMock.mockImplementation(() => Promise.resolve(mockPatchTimelineResponse)), }, }); api.getDraftTimeline(timelineType); @@ -696,7 +408,7 @@ describe('cleanDraftTimeline', () => { (KibanaServices.get as jest.Mock).mockReturnValue({ http: { - post: postMock, + post: postMock.mockImplementation(() => Promise.resolve(mockPatchTimelineResponse)), }, }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx index 6507603d30444..1f79b26394a69 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; // we don't have the types for waitFor just yet, so using "as waitFor" for when we do -import { wait as waitFor } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; import '../../../common/mock/match_media'; import { mockGlobalState, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index 80839545951d5..5c2dfa62e5951 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -14,6 +14,7 @@ import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create'; import { AnomalyResults, Anomaly } from '../../machine_learning'; +import { BuildRuleMessage } from './rule_messages'; interface BulkCreateMlSignalsParams { actions: RuleAlertAction[]; @@ -33,6 +34,7 @@ interface BulkCreateMlSignalsParams { refresh: RefreshTypes; tags: string[]; throttle: string; + buildRuleMessage: BuildRuleMessage; } interface EcsAnomaly extends Anomaly { @@ -85,6 +87,6 @@ export const bulkCreateMlSignals = async ( ): Promise => { const anomalyResults = params.someResult; const ecsResults = transformAnomalyResultsToEcs(anomalyResults); - - return singleBulkCreate({ ...params, filteredEvents: ecsResults }); + const buildRuleMessage = params.buildRuleMessage; + return singleBulkCreate({ ...params, filteredEvents: ecsResults, buildRuleMessage }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts index bdcddbf2ed21b..9eee04030a909 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts @@ -15,6 +15,7 @@ import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create'; import { SignalSearchResponse } from './types'; +import { BuildRuleMessage } from './rule_messages'; // used to generate constant Threshold Signals ID when run with the same params const NAMESPACE_ID = '0684ec03-7201-4ee0-8ee0-3a3f6b2479b2'; @@ -40,6 +41,7 @@ interface BulkCreateThresholdSignalsParams { tags: string[]; throttle: string; startedAt: Date; + buildRuleMessage: BuildRuleMessage; } interface FilterObject { @@ -195,6 +197,7 @@ export const bulkCreateThresholdSignals = async ( params.ruleParams.threshold!, params.ruleParams.ruleId ); + const buildRuleMessage = params.buildRuleMessage; - return singleBulkCreate({ ...params, filteredEvents: ecsResults }); + return singleBulkCreate({ ...params, filteredEvents: ecsResults, buildRuleMessage }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts index 604b452174045..2822568049960 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts @@ -12,6 +12,7 @@ import { singleSearchAfter } from './single_search_after'; import { AlertServices } from '../../../../../alerts/server'; import { Logger } from '../../../../../../../src/core/server'; import { SignalSearchResponse } from './types'; +import { BuildRuleMessage } from './rule_messages'; interface FindThresholdSignalsParams { from: string; @@ -21,6 +22,7 @@ interface FindThresholdSignalsParams { logger: Logger; filter: unknown; threshold: Threshold; + buildRuleMessage: BuildRuleMessage; } export const findThresholdSignals = async ({ @@ -31,6 +33,7 @@ export const findThresholdSignals = async ({ logger, filter, threshold, + buildRuleMessage, }: FindThresholdSignalsParams): Promise<{ searchResult: SignalSearchResponse; searchDuration: string; @@ -59,5 +62,6 @@ export const findThresholdSignals = async ({ logger, filter, pageSize: 0, + buildRuleMessage, }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index d369a91335347..2df180582a0ac 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -80,6 +80,7 @@ export const searchAfterAndBulkCreate = async ({ // perform search_after with optionally undefined sortId const { searchResult, searchDuration, searchErrors } = await singleSearchAfter({ + buildRuleMessage, searchAfterSortId: sortId, index: inputIndexPattern, from: tuple.from.toISOString(), @@ -153,6 +154,7 @@ export const searchAfterAndBulkCreate = async ({ success: bulkSuccess, errors: bulkErrors, } = await singleBulkCreate({ + buildRuleMessage, filteredEvents, ruleParams, services, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index f7b56f42755ab..a3b37270e50b1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -257,6 +257,7 @@ export const signalRulesAlertType = ({ enabled, refresh, tags, + buildRuleMessage, }); // The legacy ES client does not define failures when it can be present on the structure, hence why I have the & { failures: [] } const shardFailures = @@ -295,6 +296,7 @@ export const signalRulesAlertType = ({ logger, filter: esFilter, threshold, + buildRuleMessage, }); const { @@ -323,6 +325,7 @@ export const signalRulesAlertType = ({ enabled, refresh, tags, + buildRuleMessage, }); result = mergeReturns([ result, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts index 374b967d1e77f..b7cc13fd13a01 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts @@ -19,7 +19,14 @@ import { import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; import { singleBulkCreate, filterDuplicateRules } from './single_bulk_create'; import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; +import { buildRuleMessageFactory } from './rule_messages'; +const buildRuleMessage = buildRuleMessageFactory({ + id: 'fake id', + ruleId: 'fake rule id', + index: 'fakeindex', + name: 'fake name', +}); describe('singleBulkCreate', () => { const mockService: AlertServicesMock = alertsMock.createAlertServices(); @@ -158,6 +165,7 @@ describe('singleBulkCreate', () => { refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', + buildRuleMessage, }); expect(success).toEqual(true); expect(createdItemsCount).toEqual(0); @@ -192,6 +200,7 @@ describe('singleBulkCreate', () => { refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', + buildRuleMessage, }); expect(success).toEqual(true); expect(createdItemsCount).toEqual(0); @@ -218,6 +227,7 @@ describe('singleBulkCreate', () => { refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', + buildRuleMessage, }); expect(success).toEqual(true); expect(createdItemsCount).toEqual(0); @@ -245,6 +255,7 @@ describe('singleBulkCreate', () => { refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', + buildRuleMessage, }); expect(mockLogger.error).not.toHaveBeenCalled(); @@ -274,6 +285,7 @@ describe('singleBulkCreate', () => { refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', + buildRuleMessage, }); expect(mockLogger.error).toHaveBeenCalled(); expect(errors).toEqual(['[4]: internal server error']); @@ -339,6 +351,7 @@ describe('singleBulkCreate', () => { refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', + buildRuleMessage, }); expect(success).toEqual(true); expect(createdItemsCount).toEqual(1); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts index e3c3c940b3225..759890cc9d074 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts @@ -12,6 +12,7 @@ import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; import { generateId, makeFloatString, errorAggregator } from './utils'; import { buildBulkBody } from './build_bulk_body'; +import { BuildRuleMessage } from './rule_messages'; import { Logger } from '../../../../../../../src/core/server'; interface SingleBulkCreateParams { @@ -32,6 +33,7 @@ interface SingleBulkCreateParams { tags: string[]; throttle: string; refresh: RefreshTypes; + buildRuleMessage: BuildRuleMessage; } /** @@ -85,6 +87,7 @@ export interface BulkInsertSignalsResponse { // Bulk Index documents. export const singleBulkCreate = async ({ + buildRuleMessage, filteredEvents, ruleParams, services, @@ -104,9 +107,9 @@ export const singleBulkCreate = async ({ throttle, }: SingleBulkCreateParams): Promise => { filteredEvents.hits.hits = filterDuplicateRules(id, filteredEvents); - logger.debug(`about to bulk create ${filteredEvents.hits.hits.length} events`); + logger.debug(buildRuleMessage(`about to bulk create ${filteredEvents.hits.hits.length} events`)); if (filteredEvents.hits.hits.length === 0) { - logger.debug(`all events were duplicates`); + logger.debug(buildRuleMessage(`all events were duplicates`)); return { success: true, createdItemsCount: 0, errors: [] }; } // index documents after creating an ID based on the @@ -153,21 +156,27 @@ export const singleBulkCreate = async ({ body: bulkBody, }); const end = performance.now(); - logger.debug(`individual bulk process time took: ${makeFloatString(end - start)} milliseconds`); - logger.debug(`took property says bulk took: ${response.took} milliseconds`); + logger.debug( + buildRuleMessage( + `individual bulk process time took: ${makeFloatString(end - start)} milliseconds` + ) + ); + logger.debug(buildRuleMessage(`took property says bulk took: ${response.took} milliseconds`)); const createdItemsCount = countBy(response.items, 'create.status')['201'] ?? 0; const duplicateSignalsCount = countBy(response.items, 'create.status')['409']; const errorCountByMessage = errorAggregator(response, [409]); - logger.debug(`bulk created ${createdItemsCount} signals`); + logger.debug(buildRuleMessage(`bulk created ${createdItemsCount} signals`)); if (duplicateSignalsCount > 0) { - logger.debug(`ignored ${duplicateSignalsCount} duplicate signals`); + logger.debug(buildRuleMessage(`ignored ${duplicateSignalsCount} duplicate signals`)); } if (!isEmpty(errorCountByMessage)) { logger.error( - `[-] bulkResponse had errors with responses of: ${JSON.stringify(errorCountByMessage)}` + buildRuleMessage( + `[-] bulkResponse had errors with responses of: ${JSON.stringify(errorCountByMessage)}` + ) ); return { errors: Object.keys(errorCountByMessage), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts index da81911f07ad9..7b7c40f0c4355 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts @@ -12,7 +12,14 @@ import { import { singleSearchAfter } from './single_search_after'; import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; import { ShardError } from '../../types'; +import { buildRuleMessageFactory } from './rule_messages'; +const buildRuleMessage = buildRuleMessageFactory({ + id: 'fake id', + ruleId: 'fake rule id', + index: 'fakeindex', + name: 'fake name', +}); describe('singleSearchAfter', () => { const mockService: AlertServicesMock = alertsMock.createAlertServices(); @@ -32,6 +39,7 @@ describe('singleSearchAfter', () => { pageSize: 1, filter: undefined, timestampOverride: undefined, + buildRuleMessage, }); expect(searchResult).toEqual(sampleDocSearchResultsNoSortId()); }); @@ -47,6 +55,7 @@ describe('singleSearchAfter', () => { pageSize: 1, filter: undefined, timestampOverride: undefined, + buildRuleMessage, }); expect(searchErrors).toEqual([]); }); @@ -94,6 +103,7 @@ describe('singleSearchAfter', () => { pageSize: 1, filter: undefined, timestampOverride: undefined, + buildRuleMessage, }); expect(searchErrors).toEqual(['reason: some reason, type: some type, caused by: some reason']); }); @@ -110,6 +120,7 @@ describe('singleSearchAfter', () => { pageSize: 1, filter: undefined, timestampOverride: undefined, + buildRuleMessage, }); expect(searchResult).toEqual(sampleDocSearchResultsWithSortId()); }); @@ -129,6 +140,7 @@ describe('singleSearchAfter', () => { pageSize: 1, filter: undefined, timestampOverride: undefined, + buildRuleMessage, }) ).rejects.toThrow('Fake Error'); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts index f758adb21611c..3b89a2d79c0d0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts @@ -8,6 +8,7 @@ import { performance } from 'perf_hooks'; import { AlertServices } from '../../../../../alerts/server'; import { Logger } from '../../../../../../../src/core/server'; import { SignalSearchResponse } from './types'; +import { BuildRuleMessage } from './rule_messages'; import { buildEventsSearchQuery } from './build_events_query'; import { createErrorsFromShard, makeFloatString } from './utils'; import { TimestampOverrideOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; @@ -23,6 +24,7 @@ interface SingleSearchAfterParams { pageSize: number; filter: unknown; timestampOverride: TimestampOverrideOrUndefined; + buildRuleMessage: BuildRuleMessage; } // utilize search_after for paging results into bulk. @@ -37,6 +39,7 @@ export const singleSearchAfter = async ({ logger, pageSize, timestampOverride, + buildRuleMessage, }: SingleSearchAfterParams): Promise<{ searchResult: SignalSearchResponse; searchDuration: string; @@ -69,7 +72,7 @@ export const singleSearchAfter = async ({ searchErrors, }; } catch (exc) { - logger.error(`[-] nextSearchAfter threw an error ${exc}`); + logger.error(buildRuleMessage(`[-] nextSearchAfter threw an error ${exc}`)); throw exc; } }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.test.ts index 570d2fc9192c0..5a219304cea18 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.test.ts @@ -136,6 +136,29 @@ describe('Index Fields', () => { readFromDocValues: false, esTypes: [], }, + { + aggregatable: true, + category: 'agent', + esTypes: [], + indexes: ['auditbeat'], + name: 'agent.user.name', + readFromDocValues: false, + searchable: true, + type: 'string', + }, + { + aggregatable: true, + category: 'client', + description: + 'Unique number allocated to the autonomous system. The autonomous system number (ASN) uniquely identifies each network on the Internet.', + esTypes: [], + example: 15169, + indexes: ['auditbeat'], + name: 'client.as.number.text', + readFromDocValues: false, + searchable: true, + type: 'string', + }, ]) ); }); @@ -149,6 +172,7 @@ describe('Index Fields', () => { ); expect(fields).toEqual([ { + category: 'base', description: 'Each document has an _id that uniquely identifies it', example: 'Y-6TfmcB0WOhS6qyMv3s', name: '_id', @@ -156,11 +180,11 @@ describe('Index Fields', () => { searchable: true, aggregatable: false, readFromDocValues: false, - category: 'base', - indexes: ['auditbeat'], esTypes: [], + indexes: ['auditbeat'], }, { + category: 'base', description: 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', example: 'auditbeat-8.0.0-2019.02.19-000001', @@ -169,11 +193,11 @@ describe('Index Fields', () => { searchable: true, aggregatable: true, readFromDocValues: false, - category: 'base', - indexes: ['auditbeat'], esTypes: [], + indexes: ['auditbeat'], }, { + category: 'base', description: 'Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events.', example: '2016-05-23T08:05:34.853Z', @@ -181,12 +205,12 @@ describe('Index Fields', () => { type: 'date', searchable: true, aggregatable: true, - category: 'base', - indexes: ['auditbeat'], readFromDocValues: true, esTypes: [], + indexes: ['auditbeat'], }, { + category: 'agent', description: 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', example: '8a4f500f', @@ -194,12 +218,12 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: true, - category: 'agent', - indexes: ['auditbeat'], readFromDocValues: false, esTypes: [], + indexes: ['auditbeat'], }, { + category: 'agent', description: 'Custom name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', example: 'foo', @@ -207,12 +231,12 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: true, - category: 'agent', - indexes: ['auditbeat'], readFromDocValues: false, esTypes: [], + indexes: ['auditbeat'], }, { + category: 'agent', description: 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', example: 'filebeat', @@ -220,36 +244,59 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: true, - category: 'agent', - indexes: ['auditbeat'], readFromDocValues: false, esTypes: [], + indexes: ['auditbeat'], }, { + category: 'agent', description: 'Version of the agent.', example: '6.0.0-rc2', name: 'agent.version', type: 'string', searchable: true, aggregatable: true, + readFromDocValues: false, + esTypes: [], + indexes: ['auditbeat'], + }, + { category: 'agent', + name: 'agent.user.name', + searchable: true, + type: 'string', + aggregatable: true, + readFromDocValues: false, + esTypes: [], indexes: ['auditbeat'], + }, + { + category: 'client', + description: + 'Unique number allocated to the autonomous system. The autonomous system number (ASN) uniquely identifies each network on the Internet.', + example: 15169, + name: 'client.as.number.text', + type: 'string', + searchable: true, + aggregatable: true, readFromDocValues: false, esTypes: [], + indexes: ['auditbeat'], }, { + category: 'base', description: 'Each document has an _id that uniquely identifies it', example: 'Y-6TfmcB0WOhS6qyMv3s', name: '_id', type: 'string', searchable: true, aggregatable: false, - category: 'base', - indexes: ['filebeat'], readFromDocValues: false, esTypes: [], + indexes: ['filebeat'], }, { + category: 'base', description: 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', example: 'auditbeat-8.0.0-2019.02.19-000001', @@ -257,12 +304,12 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: true, - category: 'base', - indexes: ['filebeat'], readFromDocValues: false, esTypes: [], + indexes: ['filebeat'], }, { + category: 'base', description: 'Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events.', example: '2016-05-23T08:05:34.853Z', @@ -270,24 +317,24 @@ describe('Index Fields', () => { type: 'date', searchable: true, aggregatable: true, - category: 'base', - indexes: ['filebeat'], readFromDocValues: true, esTypes: [], + indexes: ['filebeat'], }, { + category: 'agent', description: 'Deprecated - use agent.name or agent.id to identify an agent. Hostname of the agent. ', name: 'agent.hostname', - searchable: true, type: 'string', + searchable: true, aggregatable: true, - category: 'agent', - indexes: ['filebeat'], readFromDocValues: false, esTypes: [], + indexes: ['filebeat'], }, { + category: 'agent', description: 'Custom name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', example: 'foo', @@ -295,36 +342,36 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: true, - category: 'agent', - indexes: ['filebeat'], readFromDocValues: false, esTypes: [], + indexes: ['filebeat'], }, { + category: 'agent', description: 'Version of the agent.', example: '6.0.0-rc2', name: 'agent.version', type: 'string', searchable: true, aggregatable: true, - category: 'agent', - indexes: ['filebeat'], readFromDocValues: false, esTypes: [], + indexes: ['filebeat'], }, { + category: 'base', description: 'Each document has an _id that uniquely identifies it', example: 'Y-6TfmcB0WOhS6qyMv3s', name: '_id', type: 'string', searchable: true, aggregatable: false, - category: 'base', - indexes: ['packetbeat'], readFromDocValues: false, esTypes: [], + indexes: ['packetbeat'], }, { + category: 'base', description: 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', example: 'auditbeat-8.0.0-2019.02.19-000001', @@ -332,12 +379,12 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: true, - category: 'base', - indexes: ['packetbeat'], readFromDocValues: false, esTypes: [], + indexes: ['packetbeat'], }, { + category: 'base', description: 'Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events.', example: '2016-05-23T08:05:34.853Z', @@ -345,12 +392,12 @@ describe('Index Fields', () => { type: 'date', searchable: true, aggregatable: true, - category: 'base', - indexes: ['packetbeat'], readFromDocValues: true, esTypes: [], + indexes: ['packetbeat'], }, { + category: 'agent', description: 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', example: '8a4f500d', @@ -358,12 +405,12 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: true, - category: 'agent', - indexes: ['packetbeat'], readFromDocValues: false, esTypes: [], + indexes: ['packetbeat'], }, { + category: 'agent', description: 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', example: 'filebeat', @@ -371,10 +418,9 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: true, - category: 'agent', - indexes: ['packetbeat'], readFromDocValues: false, esTypes: [], + indexes: ['packetbeat'], }, ]); }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts b/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts index 403a9425b221f..71b641237d6b0 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts @@ -121,8 +121,17 @@ export const createFieldItem = ( indexesAliasIdx: number ): IndexField => { const alias = indexesAlias[indexesAliasIdx]; + const splitIndexName = index.name.split('.'); + const indexName = + splitIndexName[splitIndexName.length - 1] === 'text' + ? splitIndexName.slice(0, splitIndexName.length - 1).join('.') + : index.name; + const beatIndex = fieldsBeat[indexName] ?? {}; + if (isEmpty(beatIndex.category)) { + beatIndex.category = splitIndexName[0]; + } return { - ...(fieldsBeat[index.name] ?? {}), + ...beatIndex, ...index, indexes: [alias], }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/index_fields/mock.ts b/x-pack/plugins/security_solution/server/search_strategy/index_fields/mock.ts index efb992a868f65..e5c502fe26f2c 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/index_fields/mock.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/index_fields/mock.ts @@ -48,6 +48,22 @@ export const mockAuditbeatIndexField: FieldDescriptor[] = [ readFromDocValues: false, esTypes: [], }, + { + name: 'agent.user.name', + searchable: true, + type: 'string', + aggregatable: true, + readFromDocValues: false, + esTypes: [], + }, + { + name: 'client.as.number.text', + searchable: true, + type: 'string', + aggregatable: true, + readFromDocValues: false, + esTypes: [], + }, ]; export const mockFilebeatIndexField: FieldDescriptor[] = [ diff --git a/x-pack/plugins/security_solution/server/usage/collector.ts b/x-pack/plugins/security_solution/server/usage/collector.ts index 6fadf956ccaf1..9514233bdfa86 100644 --- a/x-pack/plugins/security_solution/server/usage/collector.ts +++ b/x-pack/plugins/security_solution/server/usage/collector.ts @@ -59,10 +59,13 @@ export const registerCollector: RegisterCollector = ({ total_installed: { type: 'long' }, active_within_last_24_hours: { type: 'long' }, os: { - full_name: { type: 'keyword' }, - platform: { type: 'keyword' }, - version: { type: 'keyword' }, - count: { type: 'long' }, + type: 'array', + items: { + full_name: { type: 'keyword' }, + platform: { type: 'keyword' }, + version: { type: 'keyword' }, + count: { type: 'long' }, + }, }, policies: { malware: { diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 1236f2ad9b559..bc89d9e0c812d 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -141,15 +141,18 @@ } }, "packages": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "enabled": { - "type": "boolean" + "type": "array", + "items": { + "properties": { + "name": { + "type": "keyword" + }, + "version": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + } } } } @@ -546,78 +549,81 @@ "type": "boolean" }, "clusters": { - "properties": { - "license": { - "type": "keyword" - }, - "clusterUuid": { - "type": "keyword" - }, - "metricbeatUsed": { - "type": "boolean" - }, - "elasticsearch": { - "properties": { - "enabled": { - "type": "boolean" - }, - "count": { - "type": "long" - }, - "metricbeatUsed": { - "type": "boolean" + "type": "array", + "items": { + "properties": { + "license": { + "type": "keyword" + }, + "clusterUuid": { + "type": "keyword" + }, + "metricbeatUsed": { + "type": "boolean" + }, + "elasticsearch": { + "properties": { + "enabled": { + "type": "boolean" + }, + "count": { + "type": "long" + }, + "metricbeatUsed": { + "type": "boolean" + } } - } - }, - "kibana": { - "properties": { - "enabled": { - "type": "boolean" - }, - "count": { - "type": "long" - }, - "metricbeatUsed": { - "type": "boolean" + }, + "kibana": { + "properties": { + "enabled": { + "type": "boolean" + }, + "count": { + "type": "long" + }, + "metricbeatUsed": { + "type": "boolean" + } } - } - }, - "logstash": { - "properties": { - "enabled": { - "type": "boolean" - }, - "count": { - "type": "long" - }, - "metricbeatUsed": { - "type": "boolean" + }, + "logstash": { + "properties": { + "enabled": { + "type": "boolean" + }, + "count": { + "type": "long" + }, + "metricbeatUsed": { + "type": "boolean" + } } - } - }, - "beats": { - "properties": { - "enabled": { - "type": "boolean" - }, - "count": { - "type": "long" - }, - "metricbeatUsed": { - "type": "boolean" + }, + "beats": { + "properties": { + "enabled": { + "type": "boolean" + }, + "count": { + "type": "long" + }, + "metricbeatUsed": { + "type": "boolean" + } } - } - }, - "apm": { - "properties": { - "enabled": { - "type": "boolean" - }, - "count": { - "type": "long" - }, - "metricbeatUsed": { - "type": "boolean" + }, + "apm": { + "properties": { + "enabled": { + "type": "boolean" + }, + "count": { + "type": "long" + }, + "metricbeatUsed": { + "type": "boolean" + } } } } @@ -720,18 +726,21 @@ "type": "long" }, "os": { - "properties": { - "full_name": { - "type": "keyword" - }, - "platform": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "count": { - "type": "long" + "type": "array", + "items": { + "properties": { + "full_name": { + "type": "keyword" + }, + "platform": { + "type": "keyword" + }, + "version": { + "type": "keyword" + }, + "count": { + "type": "long" + } } } }, @@ -771,10 +780,16 @@ "type": "number" }, "enabledAuthProviders": { - "type": "keyword" + "type": "array", + "items": { + "type": "keyword" + } }, "httpAuthSchemes": { - "type": "keyword" + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -906,16 +921,28 @@ "type": "boolean" }, "autorefreshInterval": { - "type": "long" + "type": "array", + "items": { + "type": "long" + } }, "dateRangeEnd": { - "type": "date" + "type": "array", + "items": { + "type": "date" + } }, "dateRangeStart": { - "type": "date" + "type": "array", + "items": { + "type": "date" + } }, "monitor_frequency": { - "type": "long" + "type": "array", + "items": { + "type": "long" + } }, "monitor_name_stats": { "properties": { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index fd743400133a7..7dd6d478f82b0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1367,10 +1367,6 @@ "discover.embeddable.inspectorRequestDataTitle": "データ", "discover.embeddable.inspectorRequestDescription": "このリクエストはElasticsearchにクエリをかけ、検索データを取得します。", "discover.embeddable.search.displayName": "検索", - "discover.errorLoadingData": "データの読み込み中にエラーが発生", - "discover.fetchError.howToAddressErrorDescription": "このエラーは、{scriptedFields}タブにある {managementLink}の{fetchErrorScript}フィールドを編集することで解決できます。", - "discover.fetchError.managmentLinkText": "管理>インデックスパターン", - "discover.fetchError.scriptedFieldsText": "「スクリプトフィールド」", "discover.fieldChooser.detailViews.emptyStringText": "空の文字列", "discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "{field}を除外:\"{value}\"", "discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "{field}を除外:\"{value}\"", @@ -1445,7 +1441,6 @@ "discover.notifications.invalidTimeRangeTitle": "無効な時間範囲", "discover.notifications.notSavedSearchTitle": "検索「{savedSearchTitle}」は保存されませんでした。", "discover.notifications.savedSearchTitle": "検索「{savedSearchTitle}」が保存されました。", - "discover.painlessError.painlessScriptedFieldErrorMessage": "Painlessスクリプトのフィールド「{script}」のエラー.", "discover.reloadSavedSearchButton": "検索をリセット", "discover.rootBreadcrumb": "発見", "discover.savedSearch.savedObjectName": "保存検索", @@ -4380,7 +4375,6 @@ "visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "indexPatternまたはsavedSearchIdが必要です", "visualize.createVisualization.noVisTypeErrorMessage": "有効なビジュアライゼーションタイプを指定してください", "visualize.editor.createBreadcrumb": "作成", - "visualize.error.title": "ビジュアライゼーションエラー", "visualize.helpMenu.appName": "可視化", "visualize.linkedToSearch.unlinkSuccessNotificationText": "保存された検索「{searchTitle}」からリンクが解除されました", "visualize.listing.betaTitle": "ベータ", @@ -18204,7 +18198,6 @@ "xpack.triggersActionsUI.typeRegistry.register.duplicateObjectTypeErrorMessage": "オブジェクトタイプ「{id}」は既に登録されています。", "xpack.uiActionsEnhanced.components.actionWizard.changeButton": "変更", "xpack.uiActionsEnhanced.components.actionWizard.insufficientLicenseLevelTooltip": "不十分なライセンスレベル", - "xpack.uiActionsEnhanced.components.DiscoverDrilldownConfig.chooseIndexPattern": "対象インデックスパターンを選択", "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle": "パネルに追加", "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle": "キャンセル", "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel": "時間範囲", @@ -18212,7 +18205,6 @@ "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle": "更新", "xpack.uiActionsEnhanced.customizeTimeRange.modal.headerTitle": "パネルの時間範囲のカスタマイズ", "xpack.uiActionsEnhanced.customizeTimeRangeMenuItem.displayName": "時間範囲のカスタマイズ", - "xpack.uiActionsEnhanced.drilldown.goToDiscover": "Discoverに移動(例)", "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.helpText": "ドリルダウンにより、パネルと連携する新しい動作を定義できます。複数のアクションを追加し、デフォルトフィルターを無効化できます。", "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.hideHelpButtonLabel": "非表示", "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.viewDocsLinkLabel": "ドキュメントを表示", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 104fc70f5dd71..9ba587e718a08 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1368,10 +1368,6 @@ "discover.embeddable.inspectorRequestDataTitle": "数据", "discover.embeddable.inspectorRequestDescription": "此请求将查询 Elasticsearch 以获取搜索的数据。", "discover.embeddable.search.displayName": "搜索", - "discover.errorLoadingData": "加载数据时出错", - "discover.fetchError.howToAddressErrorDescription": "您可以通过编辑{managementLink}中{scriptedFields}选项卡下的“{fetchErrorScript}”字段来解决此错误。", - "discover.fetchError.managmentLinkText": "“管理”>“索引模式”", - "discover.fetchError.scriptedFieldsText": "“脚本字段”", "discover.fieldChooser.detailViews.emptyStringText": "空字符串", "discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "筛除 {field}:“{value}”", "discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "筛留 {field}:“{value}”", @@ -1446,7 +1442,6 @@ "discover.notifications.invalidTimeRangeTitle": "时间范围无效", "discover.notifications.notSavedSearchTitle": "搜索“{savedSearchTitle}”未保存。", "discover.notifications.savedSearchTitle": "搜索“{savedSearchTitle}”已保存", - "discover.painlessError.painlessScriptedFieldErrorMessage": "Painless 脚本字段“{script}”有错误。", "discover.reloadSavedSearchButton": "重置搜索", "discover.rootBreadcrumb": "Discover", "discover.savedSearch.savedObjectName": "已保存搜索", @@ -4381,7 +4376,6 @@ "visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "必须提供 indexPattern 或 savedSearchId", "visualize.createVisualization.noVisTypeErrorMessage": "必须提供有效的可视化类型", "visualize.editor.createBreadcrumb": "创建", - "visualize.error.title": "可视化错误", "visualize.helpMenu.appName": "Visualize", "visualize.linkedToSearch.unlinkSuccessNotificationText": "已取消与已保存搜索“{searchTitle}”的链接", "visualize.listing.betaTitle": "公测版", @@ -18215,7 +18209,6 @@ "xpack.triggersActionsUI.typeRegistry.register.duplicateObjectTypeErrorMessage": "已注册对象类型“{id}”。", "xpack.uiActionsEnhanced.components.actionWizard.changeButton": "更改", "xpack.uiActionsEnhanced.components.actionWizard.insufficientLicenseLevelTooltip": "许可证级别不够", - "xpack.uiActionsEnhanced.components.DiscoverDrilldownConfig.chooseIndexPattern": "选择目标索引模式", "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle": "添加到面板", "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle": "取消", "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel": "时间范围", @@ -18223,7 +18216,6 @@ "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle": "更新", "xpack.uiActionsEnhanced.customizeTimeRange.modal.headerTitle": "定制面板时间范围", "xpack.uiActionsEnhanced.customizeTimeRangeMenuItem.displayName": "定制时间范围", - "xpack.uiActionsEnhanced.drilldown.goToDiscover": "前往 Discover(示例)", "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.helpText": "向下钻取允许您定义与面板交互的新行为。您可以添加多个操作并覆盖默认筛选。", "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.hideHelpButtonLabel": "隐藏", "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.viewDocsLinkLabel": "查看文档", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx index 0674e5b35c61f..059bae6e3ebff 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx @@ -91,6 +91,7 @@ describe('pagerduty action params validation', () => { expect(actionTypeModel.validateParams(actionParams)).toEqual({ errors: { + dedupKey: [], summary: [], timestamp: [], }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx index 90d8da346c71d..03bfbb38da6f2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx @@ -50,8 +50,22 @@ export function getActionType(): ActionTypeModel { const errors = { summary: new Array(), timestamp: new Array(), + dedupKey: new Array(), }; validationResult.errors = errors; + if ( + !actionParams.dedupKey?.length && + (actionParams.eventAction === 'resolve' || actionParams.eventAction === 'acknowledge') + ) { + errors.dedupKey.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredDedupKeyText', + { + defaultMessage: 'DedupKey is required when resolving or acknowledging an incident.', + } + ) + ); + } if (!actionParams.summary?.length) { errors.summary.push( i18n.translate( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx index 6a11dc8d0d6a5..fe83054edbe07 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx @@ -26,7 +26,7 @@ describe('PagerDutyParamsFields renders', () => { const wrapper = mountWithIntl( {}} index={0} docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx index c8ad5f5b7080e..39800865ed761 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx @@ -94,6 +94,9 @@ const PagerDutyParamsFields: React.FunctionComponent @@ -144,12 +147,23 @@ const PagerDutyParamsFields: React.FunctionComponent 0} + label={ + isDedupeKeyRequired + ? i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.dedupKeyTextRequiredFieldLabel', + { + defaultMessage: 'DedupKey', + } + ) + : i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.dedupKeyTextFieldLabel', + { + defaultMessage: 'DedupKey (optional)', + } + ) + } > > { const { body } = req; - let dedupKey = body && body.dedup_key; - const summary = body && body.payload && body.payload.summary; - - if (dedupKey == null) { - dedupKey = `kibana-ft-simulator-dedup-key-${new Date().toISOString()}`; - } + const summary = body?.payload?.summary; switch (summary) { case 'respond-with-429': @@ -67,7 +62,7 @@ export function initPlugin(router: IRouter, path: string) { return jsonResponse(res, 202, { status: 'success', message: 'Event processed', - dedup_key: dedupKey, + ...(body?.dedup_key ? { dedup_key: body?.dedup_key } : {}), }); } ); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts index 0c4d9096aa31a..caa1884636007 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts @@ -163,7 +163,6 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { status: 'ok', actionId: simulatedActionId, data: { - dedup_key: `action:${simulatedActionId}`, message: 'Event processed', status: 'success', }, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts index 81f7c8c97ba8c..17070a14069ce 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts @@ -39,5 +39,48 @@ export default function createGetTests({ getService }: FtrProviderContext) { expect(response.status).to.eql(200); expect(response.body.consumer).to.equal('infrastructure'); }); + + it('7.10.0 migrates PagerDuty actions to have a default dedupKey', async () => { + const response = await supertest.get( + `${getUrlPrefix(``)}/api/alerts/alert/b6087f72-994f-46fb-8120-c6e5c50d0f8f` + ); + + expect(response.status).to.eql(200); + + expect(response.body.actions).to.eql([ + { + actionTypeId: '.pagerduty', + id: 'a6a8ab7a-35cf-445e-ade3-215a029c2ee3', + group: 'default', + params: { + component: '', + eventAction: 'trigger', + summary: 'fired {{alertInstanceId}}', + }, + }, + { + actionTypeId: '.pagerduty', + id: 'a6a8ab7a-35cf-445e-ade3-215a029c2ee3', + group: 'default', + params: { + component: '', + dedupKey: '{{alertId}}', + eventAction: 'resolve', + summary: 'fired {{alertInstanceId}}', + }, + }, + { + actionTypeId: '.pagerduty', + id: 'a6a8ab7a-35cf-445e-ade3-215a029c2ee3', + group: 'default', + params: { + component: '', + dedupKey: '{{alertInstanceId}}', + eventAction: 'resolve', + summary: 'fired {{alertInstanceId}}', + }, + }, + ]); + }); }); } diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js index b74df71701026..bd35374643e9b 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js @@ -40,21 +40,18 @@ export default function ({ getPageObjects, getService }) { operation: 'date_histogram', field: '@timestamp', }); - await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', operation: 'avg', field: 'bytes', }); - await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', operation: 'terms', field: 'ip', }); - await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.save(title, saveAsNew, redirectToOrigin); } diff --git a/x-pack/test/functional/apps/discover/error_handling.ts b/x-pack/test/functional/apps/discover/error_handling.ts index 515e5e293ae28..40aa8cd5c0606 100644 --- a/x-pack/test/functional/apps/discover/error_handling.ts +++ b/x-pack/test/functional/apps/discover/error_handling.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const testSubjects = getService('testSubjects'); + const toasts = getService('toasts'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); describe('errors', function describeIndexTests() { @@ -23,11 +23,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async function () { await esArchiver.unload('invalid_scripted_field'); }); + // this is the same test as in OSS but it catches different error message issue in different licences describe('invalid scripted field error', () => { it('is rendered', async () => { - const isFetchErrorVisible = await testSubjects.exists('discoverFetchError'); - expect(isFetchErrorVisible).to.be(true); + const toast = await toasts.getToastElement(1); + const painlessStackTrace = await toast.findByTestSubject('painlessStackTrace'); + expect(painlessStackTrace).not.to.be(undefined); }); }); }); diff --git a/x-pack/test/functional/apps/lens/rollup.ts b/x-pack/test/functional/apps/lens/rollup.ts index 8e1dc231b6b1a..f6882c8aed214 100644 --- a/x-pack/test/functional/apps/lens/rollup.ts +++ b/x-pack/test/functional/apps/lens/rollup.ts @@ -34,21 +34,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { operation: 'date_histogram', field: '@timestamp', }); - await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', operation: 'sum', field: 'bytes', }); - await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', operation: 'terms', field: 'geo.src', }); - await PageObjects.lens.closeDimensionEditor(); expect(await find.allByCssSelector('.echLegendItem')).to.have.length(2); await PageObjects.lens.save('Afancilenstest'); diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 05047fab2517d..d26c92a2bcd63 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -25,21 +25,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { operation: 'date_histogram', field: '@timestamp', }); - await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', operation: 'avg', field: 'bytes', }); - await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', operation: 'terms', field: '@message.raw', }); - await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.switchToVisualization('lnsDatatable'); await PageObjects.lens.removeDimension('lnsDatatable_column'); @@ -50,7 +47,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { operation: 'terms', field: 'ip', }); - await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.save('Afancilenstest'); @@ -67,18 +63,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // legend item(s), so we're using a class selector here. expect(await find.allByCssSelector('.echLegendItem')).to.have.length(3); }); + it('should create an xy visualization with filters aggregation', async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); await listingTable.searchForItemWithName('lnsXYvis'); await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); await PageObjects.lens.goToTimeRange(); + // Change the IP field to filters await PageObjects.lens.configureDimension({ dimension: 'lnsXY_splitDimensionPanel > lns-dimensionTrigger', operation: 'filters', isPreviousIncompatible: true, + keepOpen: true, }); await PageObjects.lens.addFilterToAgg(`geo.src : CN`); + // Verify that the field was persisted from the transition expect(await PageObjects.lens.getFiltersAggLabels()).to.eql([`ip : *`, `geo.src : CN`]); expect(await find.allByCssSelector('.echLegendItem')).to.have.length(2); }); @@ -107,14 +107,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { field: '@timestamp', }); - await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', operation: 'avg', field: 'bytes', }); - await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.createLayer(); expect(await PageObjects.lens.hasChartSwitchWarning('line')).to.eql(false); @@ -129,7 +127,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 1 ); - await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension( { dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', @@ -139,7 +136,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 1 ); - await PageObjects.lens.closeDimensionEditor(); expect(await PageObjects.lens.getLayerCount()).to.eql(2); await testSubjects.click('lnsLayerRemove'); await testSubjects.click('lnsLayerRemove'); @@ -168,8 +164,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.existOrFail('indexPattern-dimension-formatDecimals'); - await PageObjects.lens.closeDimensionEditor(); - expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( 'Test of label' ); @@ -186,14 +180,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { field: 'geo.dest', }); - await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', operation: 'avg', field: 'bytes', }); - await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.createLayer(); await PageObjects.lens.configureDimension( @@ -205,7 +197,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 1 ); - await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension( { dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', @@ -215,7 +206,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 1 ); - await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.save('twolayerchart'); await testSubjects.click('lnsSuggestion-asDonut > lnsSuggestion'); @@ -301,7 +291,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { operation: 'date_histogram', field: '@timestamp', }); - await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension({ dimension: 'lnsPie_sizeByDimensionPanel > lns-empty-dimension', @@ -309,7 +298,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { field: 'bytes', }); - await PageObjects.lens.closeDimensionEditor(); expect(await PageObjects.lens.hasChartSwitchWarning('lnsDatatable')).to.eql(false); await PageObjects.lens.switchToVisualization('lnsDatatable'); diff --git a/x-pack/test/functional/es_archives/alerts/data.json b/x-pack/test/functional/es_archives/alerts/data.json index cc246b0fe44da..4e879116d8cda 100644 --- a/x-pack/test/functional/es_archives/alerts/data.json +++ b/x-pack/test/functional/es_archives/alerts/data.json @@ -80,4 +80,115 @@ "updated_at": "2020-06-17T15:35:39.839Z" } } +} + +{ + "type": "doc", + "value": { + "id": "action:a6a8ab7a-35cf-445e-ade3-215a029c2ee3", + "index": ".kibana_1", + "source": { + "action": { + "actionTypeId": ".pagerduty", + "config": { + "apiUrl": "http://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/pagerduty" + }, + "name": "A pagerduty action", + "secrets": "kvjaTWYKGmCqptyv4giaN+nQGgsZrKXmlULcbAP8KK3JmR8Ei9ADqh5mB+uVC+x+Q7/vTQ5SKZCj3dHv3pmNzZ5WGyZYQFBaaa63Mkp3kIcnpE1OdSAv+3Z/Y+XihHAM19zUm3JRpojnIpYegoS5/vMx1sOzcf/+miYUuZw2lgo0lNE=" + }, + "references": [ + ], + "type": "action", + "updated_at": "2020-09-22T15:16:06.924Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "alert:b6087f72-994f-46fb-8120-c6e5c50d0f8f", + "index": ".kibana_1", + "source": { + "alert": { + "actions": [ + { + "actionRef": "action_0", + "actionTypeId": ".pagerduty", + "group": "default", + "params": { + "component": "", + "eventAction": "trigger", + "summary": "fired {{alertInstanceId}}" + } + }, + { + "actionRef": "action_1", + "actionTypeId": ".pagerduty", + "group": "default", + "params": { + "component": "", + "eventAction": "resolve", + "summary": "fired {{alertInstanceId}}" + } + }, + { + "actionRef": "action_2", + "actionTypeId": ".pagerduty", + "group": "default", + "params": { + "component": "", + "dedupKey": "{{alertInstanceId}}", + "eventAction": "resolve", + "summary": "fired {{alertInstanceId}}" + } + } + ], + "alertTypeId": "test.noop", + "apiKey": null, + "apiKeyOwner": null, + "consumer": "alertsFixture", + "createdAt": "2020-09-22T15:16:07.451Z", + "createdBy": null, + "enabled": true, + "muteAll": false, + "mutedInstanceIds": [ + ], + "name": "abc", + "params": { + }, + "schedule": { + "interval": "1m" + }, + "scheduledTaskId": "8a7c6ff0-fce6-11ea-a888-9337d77a2c25", + "tags": [ + "foo" + ], + "throttle": "1m", + "updatedBy": null + }, + "migrationVersion": { + "alert": "7.9.0" + }, + "references": [ + { + "id": "a6a8ab7a-35cf-445e-ade3-215a029c2ee3", + "name": "action_0", + "type": "action" + }, + { + "id": "a6a8ab7a-35cf-445e-ade3-215a029c2ee3", + "name": "action_1", + "type": "action" + }, + { + "id": "a6a8ab7a-35cf-445e-ade3-215a029c2ee3", + "name": "action_2", + "type": "action" + } + ], + "type": "alert", + "updated_at": "2020-09-22T15:16:08.456Z" + } + } } \ No newline at end of file diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index a1e62afbe14c8..ec7281e53c5e1 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -90,6 +90,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont operation: string; field?: string; isPreviousIncompatible?: boolean; + keepOpen?: boolean; }, layerIndex = 0 ) { @@ -107,6 +108,10 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await comboBox.openOptionsList(target); await comboBox.setElement(target, opts.field); } + + if (!opts.keepOpen) { + this.closeDimensionEditor(); + } }, // closes the dimension editor flyout @@ -127,7 +132,16 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await testSubjects.click('lns-newBucket-add'); const queryInput = await testSubjects.find('indexPattern-filters-queryStringInput'); await queryInput.type(queryString); - await PageObjects.common.pressEnterKey(); + // Problem here is that after typing in the queryInput a dropdown will fetch the server + // with suggestions and show up. Depending on the cursor position and some other factors + // pressing Enter at this point may lead to auto-complete the queryInput with random stuff from the + // dropdown which was not intended originally. + // To close the Filter popover we need to move to the label input and then press Enter: + // solution is to press Tab 2 twice (first Tab will close the dropdown) instead of Enter to avoid + // race condition with the dropdown + await PageObjects.common.pressTabKey(); + await PageObjects.common.pressTabKey(); + // Now it is safe to press Enter as we're in the label input await PageObjects.common.pressEnterKey(); await PageObjects.common.sleep(1000); // give time for debounced components to rerender }, diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/bulk_upgrade.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/bulk_upgrade.ts index e377ea5a762f9..bafcb79a419c2 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/bulk_upgrade.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/bulk_upgrade.ts @@ -10,7 +10,7 @@ import { skipIfNoDockerRegistry } from '../../helpers'; import { BulkInstallPackageInfo, BulkInstallPackagesResponse, - IBulkInstallPackageError, + IBulkInstallPackageHTTPError, } from '../../../../plugins/ingest_manager/common'; export default function (providerContext: FtrProviderContext) { @@ -68,7 +68,7 @@ export default function (providerContext: FtrProviderContext) { expect(entry.oldVersion).equal('0.1.0'); expect(entry.newVersion).equal('0.3.0'); - const err = body.response[1] as IBulkInstallPackageError; + const err = body.response[1] as IBulkInstallPackageHTTPError; expect(err.statusCode).equal(404); expect(body.response[1].name).equal('blahblah'); }); diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js index e509babc9828b..0cb998b9b7c35 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js @@ -7,6 +7,7 @@ export default function loadTests({ loadTestFile }) { describe('EPM Endpoints', () => { loadTestFile(require.resolve('./list')); + loadTestFile(require.resolve('./setup')); loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./file')); //loadTestFile(require.resolve('./template')); diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/setup.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/setup.ts new file mode 100644 index 0000000000000..da06f49dd6139 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/setup.ts @@ -0,0 +1,48 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; +import { GetInfoResponse, Installed } from '../../../../plugins/ingest_manager/common'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const log = getService('log'); + + describe('setup api', async () => { + skipIfNoDockerRegistry(providerContext); + describe('setup performs upgrades', async () => { + const oldEndpointVersion = '0.13.0'; + beforeEach(async () => { + await supertest + .post(`/api/ingest_manager/epm/packages/endpoint-${oldEndpointVersion}`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }); + it('upgrades the endpoint package from 0.13.0 to the latest version available', async function () { + let { body }: { body: GetInfoResponse } = await supertest + .get(`/api/ingest_manager/epm/packages/endpoint-${oldEndpointVersion}`) + .expect(200); + const latestEndpointVersion = body.response.latestVersion; + log.info(`Endpoint package latest version: ${latestEndpointVersion}`); + // make sure we're actually doing an upgrade + expect(latestEndpointVersion).not.eql(oldEndpointVersion); + await supertest.post(`/api/ingest_manager/setup`).set('kbn-xsrf', 'xxxx').expect(200); + + ({ body } = await supertest + .get(`/api/ingest_manager/epm/packages/endpoint-${latestEndpointVersion}`) + .expect(200)); + expect(body.response).to.have.property('savedObject'); + expect((body.response as Installed).savedObject.attributes.install_version).to.eql( + latestEndpointVersion + ); + }); + }); + }); +} diff --git a/x-pack/test/plugin_api_perf/plugins/task_manager_performance/package.json b/x-pack/test/plugin_api_perf/plugins/task_manager_performance/package.json index 542c8358114f4..1eb524e71da19 100644 --- a/x-pack/test/plugin_api_perf/plugins/task_manager_performance/package.json +++ b/x-pack/test/plugin_api_perf/plugins/task_manager_performance/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "uuid": "3.3.2", - "stats-lite": "2.2.0", + "stats-lite": "^2.2.0", "pretty-ms": "5.0.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 7aa88bcc88348..b6516b8bf3b55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,9 +3,9 @@ "@babel/cli@^7.10.5": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.10.5.tgz#57df2987c8cf89d0fc7d4b157ec59d7619f1b77a" - integrity sha512-j9H9qSf3kLdM0Ao3aGPbGZ73mEA9XazuupcS6cDGWuiyAcANoguhP0r2Lx32H5JGw4sSSoHG3x/mxVnHgvOoyA== + version "7.11.6" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.11.6.tgz#1fcbe61c2a6900c3539c06ee58901141f3558482" + integrity sha512-+w7BZCvkewSmaRM6H4L2QM3RL90teqEIHDIFXAmrW33+0jhlymnDAEdqVeCZATvxhQuio1ifoGVlJJbIiH9Ffg== dependencies: commander "^4.0.1" convert-source-map "^1.1.0" @@ -1212,28 +1212,6 @@ resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.0.2.tgz#56b9ef03984a05cc213772ae3713ea8ef47b0314" integrity sha512-IoxURM5zraoQ7C8f+mJb9HYSENiZGgRVcG4tLQxE61yHNNRDXtGDWTZh8N1KIHcsqN1CEPETjuzBXkJYF/fDiQ== -"@elastic/eui@0.0.55": - version "0.0.55" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-0.0.55.tgz#95ff5b70ecb1d333e1a5570a0e4a4079fdf7887b" - integrity sha512-U+YElZOnWEHcLyyfN4hk4LB3znuIR5V6VruPisMXymu/seKrvND2pJmMaB0nP8a1SVa8KufboXrGEDzBWJ4TUg== - dependencies: - classnames "^2.2.5" - core-js "^2.5.1" - focus-trap-react "^3.0.4" - highlight.js "^9.12.0" - html "^1.0.0" - keymirror "^0.1.1" - lodash "^3.10.1" - numeral "^2.0.6" - prop-types "^15.6.0" - react-ace "^5.5.0" - react-color "^2.13.8" - react-datepicker v1.4.1 - react-input-autosize "^2.2.1" - react-virtualized "^9.18.5" - tabbable "^1.1.0" - uuid "^3.1.0" - "@elastic/eui@29.0.0": version "29.0.0" resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-29.0.0.tgz#1c8d822c62ad5e29298a3a36f5b02fd9b32a5550" @@ -3888,7 +3866,7 @@ "@types/glob" "*" "@types/node" "*" -"@types/glob@*", "@types/glob@^5.0.35": +"@types/glob@*": version "5.0.35" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-5.0.35.tgz#1ae151c802cece940443b5ac246925c85189f32a" integrity sha512-wc+VveszMLyMWFvXLkloixT4n0harUIVZjnpzztaZ0nKLuul7Z32iMt2fUFGAaZ4y1XWjFRMtCI5ewvyh4aIeg== @@ -3906,12 +3884,13 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/globby@^6.1.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@types/globby/-/globby-6.1.0.tgz#7c25b975512a89effea2a656ca8cf6db7fb29d11" - integrity sha512-j3XSDNoK4LO5T+ZviQD6PqfEjm07QFEacOTbJR3hnLWuWX0ZMLJl9oRPgj1PyrfGbXhfHFkksC9QZ9HFltJyrw== +"@types/glob@^7.1.2": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" + integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== dependencies: - "@types/glob" "*" + "@types/minimatch" "*" + "@types/node" "*" "@types/globby@^8.0.0": version "8.0.0" @@ -4056,14 +4035,6 @@ dependencies: "@types/hapi" "*" -"@types/inquirer@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-6.5.0.tgz#b83b0bf30b88b8be7246d40e51d32fe9d10e09be" - integrity sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw== - dependencies: - "@types/through" "*" - rxjs "^6.4.0" - "@types/inquirer@^7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-7.3.1.tgz#1f231224e7df11ccfaf4cf9acbcc3b935fea292d" @@ -4492,10 +4463,10 @@ resolved "https://registry.yarnpkg.com/@types/pegjs/-/pegjs-0.10.1.tgz#9a2f3961dc62430fdb21061eb0ddbd890f9e3b94" integrity sha512-ra8IchO9odGQmYKbm+94K58UyKCEKdZh9y0vxhG4pIpOJOBlC1C+ZtBVr6jLs+/oJ4pl+1p/4t3JtBA8J10Vvw== -"@types/pngjs@^3.3.2": - version "3.3.2" - resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-3.3.2.tgz#8ed3bd655ab3a92ea32ada7a21f618e63b93b1d4" - integrity sha512-/SBsv93rVnjByzcau24rBwb+N7BHFp2LateaXz1e7m7M0Wzck/ymXTNdWVrCtkuMbwTHAnfdc3X/I/5szsTEAA== +"@types/pngjs@^3.4.0": + version "3.4.2" + resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-3.4.2.tgz#8dc49b45fbcf18a5873179e3664f049388e39ecf" + integrity sha512-LJVPDraJ5YFEnMHnzxTN4psdWz1M61MtaAAWPn3qnDk5fvs7BAmmQ9pd3KPlrdrvozMyne4ktanD4pg0L7x1Pw== dependencies: "@types/node" "*" @@ -5432,13 +5403,6 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -abort-controller@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-2.0.3.tgz#b174827a732efadff81227ed4b8d1cc569baf20a" - integrity sha512-EPSq5wr2aFyAZ1PejJB32IX9Qd4Nwus+adnp7STYFM5/23nLPBazqZ1oor6ZqbH+4otaaGXTlC8RN5hq3C8w9Q== - dependencies: - event-target-shim "^5.0.0" - abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -5753,7 +5717,7 @@ angular-route@^1.8.0: resolved "https://registry.yarnpkg.com/angular-route/-/angular-route-1.8.0.tgz#cb8066c5d34284ffd6a15ac7be1b3d51c5ad7bb2" integrity sha512-ORvXAdVfCCA6XFwyjSkVQFFGufj0mNGiCvBR93Qsii8+7t/6Ioy6wheUoCj1x4NWUv7hAq3nYYGCVO6QEKb1BQ== -angular-sanitize@1.8.0, angular-sanitize@^1.8.0: +angular-sanitize@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/angular-sanitize/-/angular-sanitize-1.8.0.tgz#9f80782d3afeec3bcc0bb92b3ca6f1f421cfbca6" integrity sha512-j5GiOPCvfcDWK5svEOVoPb11X3UDVy/mdHPRWuy14Iyw86xaq+Bb+x/em2sAOa5MQQeY5ciLXbF3RRp8iCKcNg== @@ -6244,7 +6208,7 @@ archiver-utils@^2.1.0: normalize-path "^3.0.0" readable-stream "^2.0.0" -archiver@3.1.1, archiver@^3.1.1: +archiver@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/archiver/-/archiver-3.1.1.tgz#9db7819d4daf60aec10fe86b16cb9258ced66ea0" integrity sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg== @@ -8667,7 +8631,7 @@ class-utils@^0.3.5: lazy-cache "^2.0.2" static-extend "^0.1.1" -classnames@2.2.6, classnames@^2.2.3, classnames@^2.2.5, classnames@^2.2.6: +classnames@2.2.6, classnames@^2.2.5, classnames@^2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== @@ -9172,11 +9136,6 @@ commander@2.17.x, commander@~2.17.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== -commander@3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" - integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== - commander@^2.13.0, commander@^2.15.1, commander@^2.16.0, commander@^2.19.0: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" @@ -9192,10 +9151,10 @@ commander@^2.8.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" integrity sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ== -commander@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.0.tgz#0641ea00838c7a964627f04cddc336a2deddd60a" - integrity sha512-pl3QrGOBa9RZaslQiqnnKX2J068wcQw7j9AIaBQ9/JEp5RY6je4jKTImg0Bd+rpoONSe7GUFSgkxLeo17m3Pow== +commander@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" + integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== commander@^4.0.1, commander@^4.1.1: version "4.1.1" @@ -9588,7 +9547,7 @@ core-js@^1.0.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= -core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.1, core-js@^2.5.3, core-js@^2.6.5, core-js@^2.6.9: +core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.3, core-js@^2.6.5, core-js@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== @@ -11113,11 +11072,6 @@ dom-converter@~0.2: dependencies: utila "~0.4" -"dom-helpers@^2.4.0 || ^3.0.0": - version "3.3.1" - resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6" - integrity sha512-2Sm+JaYn74OiTM2wHvxJOo3roiq/h25Yi69Fqk269cNUwIXsCvATB6CRSFC9Am/20G2b28hGv/+7NiWydIrPvg== - dom-helpers@^5.0.0: version "5.1.3" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.1.3.tgz#7233248eb3a2d1f74aafca31e52c5299cc8ce821" @@ -13374,7 +13328,7 @@ focus-lock@^0.7.0: resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.7.0.tgz#b2bfb0ca7beacc8710a1ff74275fe0dc60a1d88a" integrity sha512-LI7v2mH02R55SekHYdv9pRHR9RajVNyIJ2N5IEkWbg7FT5ZmJ9Hw4mWxHeEUcd+dJo0QmzztHvDvWcc7prVFsw== -focus-trap-react@^3.0.4, focus-trap-react@^3.1.1: +focus-trap-react@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/focus-trap-react/-/focus-trap-react-3.1.2.tgz#4dd021ccd028bbd3321147d132cdf7585d6d1394" integrity sha512-MoQmONoy9gRPyrC5DGezkcOMGgx7MtIOAQDHe098UtL2sA2vmucJwEmQisb+8LRXNYFHxuw5zJ1oLFeKu4Mteg== @@ -13935,11 +13889,6 @@ getobject@~0.1.0: resolved "https://registry.yarnpkg.com/getobject/-/getobject-0.1.0.tgz#047a449789fa160d018f5486ed91320b6ec7885c" integrity sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw= -getopts@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.2.4.tgz#3137fe8a5fddf304904059a851bdc1c22f0f54fb" - integrity sha512-Rz7DGyomZjrenu9Jx4qmzdlvJgvrEFHXHvjK0FcZtcTC1U5FmES7OdZHUwMuSnEE6QvBvwse1JODKj7TgbSEjQ== - getopts@^2.2.5: version "2.2.5" resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.2.5.tgz#67a0fe471cacb9c687d817cab6450b96dde8313b" @@ -14007,7 +13956,7 @@ gl-matrix@^3.2.1: resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.3.0.tgz#232eef60b1c8b30a28cbbe75b2caf6c48fd6358b" integrity sha512-COb7LDz+SXaHtl/h4LeaFcNdJdAQSDeVqjiIihSXNrkWObZLhDI4hIkZC11Aeqp7bcE72clzB0BnDXr2SmslRA== -glob-all@^3.1.0, glob-all@^3.2.1: +glob-all@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/glob-all/-/glob-all-3.2.1.tgz#082ca81afd2247cbd3ed2149bb2630f4dc877d95" integrity sha512-x877rVkzB3ipid577QOp+eQCR6M5ZyiwrtaYgrX/z3EThaSPFtLDwBXFHc3sH1cG0R0vFYI5SRYeWMMSEyXkUw== @@ -15385,7 +15334,17 @@ history-extra@^5.0.1: resolved "https://registry.yarnpkg.com/history-extra/-/history-extra-5.0.1.tgz#95a2e59dda526c4241d0ae1b124a77a5e4675ce8" integrity sha512-6XV1L1lHgporVWgppa/Kq+Fnz4lhBew7iMxYCTfzVmoEywsAKJnTjdw1zOd+EGLHGYp0/V8jSVMEgqx4QbHLTw== -history@4.9.0, history@^4.9.0: +history@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/history/-/history-3.3.0.tgz#fcedcce8f12975371545d735461033579a6dae9c" + integrity sha1-/O3M6PEpdTcVRdc1RhAzV5ptrpw= + dependencies: + invariant "^2.2.1" + loose-envify "^1.2.0" + query-string "^4.2.2" + warning "^3.0.0" + +history@^4.9.0: version "4.9.0" resolved "https://registry.yarnpkg.com/history/-/history-4.9.0.tgz#84587c2068039ead8af769e9d6a6860a14fa1bca" integrity sha512-H2DkjCjXf0Op9OAr6nJ56fcRkTSNrUiv41vNJ6IswJjif6wlpZK0BTfFbi7qK9dXLSYZxkq5lBsj3vUjlYBYZA== @@ -15397,16 +15356,6 @@ history@4.9.0, history@^4.9.0: tiny-warning "^1.0.0" value-equal "^0.4.0" -history@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/history/-/history-3.3.0.tgz#fcedcce8f12975371545d735461033579a6dae9c" - integrity sha1-/O3M6PEpdTcVRdc1RhAzV5ptrpw= - dependencies: - invariant "^2.2.1" - loose-envify "^1.2.0" - query-string "^4.2.2" - warning "^3.0.0" - hjson@3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/hjson/-/hjson-3.2.1.tgz#20de41dc87fc9a10d1557d0230b0e02afb1b09ac" @@ -15541,7 +15490,7 @@ html-webpack-plugin@^4.0.0-beta.2: tapable "^1.1.0" util.promisify "1.0.0" -html@1.0.0, html@^1.0.0: +html@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/html/-/html-1.0.0.tgz#a544fa9ea5492bfb3a2cca8210a10be7b5af1f61" integrity sha1-pUT6nqVJK/s6LMqCEKEL57WvH2E= @@ -18359,7 +18308,7 @@ kew@~0.1.7: resolved "https://registry.yarnpkg.com/kew/-/kew-0.1.7.tgz#0a32a817ff1a9b3b12b8c9bacf4bc4d679af8e72" integrity sha1-CjKoF/8amzsSuMm6z0vE1nmvjnI= -keymirror@0.1.1, keymirror@^0.1.1: +keymirror@0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/keymirror/-/keymirror-0.1.1.tgz#918889ea13f8d0a42e7c557250eee713adc95c35" integrity sha1-kYiJ6hP40KQufFVyUO7nE63JXDU= @@ -19180,11 +19129,6 @@ lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== -lodash@^3.10.1: - version "3.10.1" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" - integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y= - log-ok@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/log-ok/-/log-ok-0.1.1.tgz#bea3dd36acd0b8a7240d78736b5b97c65444a334" @@ -19367,14 +19311,6 @@ lowlight@~1.9.1: dependencies: highlight.js "~9.12.0" -lru-cache@4.1.5, lru-cache@^4.0.1, lru-cache@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - lru-cache@4.1.x, lru-cache@^4.0.0: version "4.1.2" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.2.tgz#45234b2e6e2f2b33da125624c4664929a0224c3f" @@ -19383,6 +19319,14 @@ lru-cache@4.1.x, lru-cache@^4.0.0: pseudomap "^1.0.2" yallist "^2.1.2" +lru-cache@^4.0.1, lru-cache@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -20044,20 +19988,20 @@ mini-create-react-context@^0.4.0: "@babel/runtime" "^7.5.5" tiny-warning "^1.0.3" -mini-css-extract-plugin@0.7.0, mini-css-extract-plugin@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.7.0.tgz#5ba8290fbb4179a43dd27cca444ba150bee743a0" - integrity sha512-RQIw6+7utTYn8DBGsf/LpRgZCJMpZt+kuawJ/fju0KiOL6nAaTBNmCJwS7HtwSCXfS47gCkmtBFS7HdsquhdxQ== +mini-css-extract-plugin@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz#81d41ec4fe58c713a96ad7c723cdb2d0bd4d70e1" + integrity sha512-MNpRGbNA52q6U92i0qbVpQNsgk7LExy41MdAlG84FeytfDOtRIf/mCHdEgG8rpTKOaNKiqUnZdlptF469hxqOw== dependencies: loader-utils "^1.1.0" normalize-url "1.9.1" schema-utils "^1.0.0" webpack-sources "^1.1.0" -mini-css-extract-plugin@0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz#81d41ec4fe58c713a96ad7c723cdb2d0bd4d70e1" - integrity sha512-MNpRGbNA52q6U92i0qbVpQNsgk7LExy41MdAlG84FeytfDOtRIf/mCHdEgG8rpTKOaNKiqUnZdlptF469hxqOw== +mini-css-extract-plugin@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.7.0.tgz#5ba8290fbb4179a43dd27cca444ba150bee743a0" + integrity sha512-RQIw6+7utTYn8DBGsf/LpRgZCJMpZt+kuawJ/fju0KiOL6nAaTBNmCJwS7HtwSCXfS47gCkmtBFS7HdsquhdxQ== dependencies: loader-utils "^1.1.0" normalize-url "1.9.1" @@ -20365,15 +20309,10 @@ moment-timezone@^0.5.27: dependencies: moment ">= 2.9.0" -"moment@>= 2.9.0", moment@>=1.6.0, moment@>=2.14.0, moment@^2.10.6, moment@^2.24.0: - version "2.24.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" - integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== - -moment@^2.19.3, moment@^2.27.0: - version "2.27.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" - integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== +"moment@>= 2.9.0", moment@>=1.6.0, moment@>=2.14.0, moment@^2.10.6, moment@^2.19.3, moment@^2.24.0, moment@^2.27.0: + version "2.28.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.28.0.tgz#cdfe73ce01327cee6537b0fafac2e0f21a237d75" + integrity sha512-Z5KOjYmnHyd/ukynmFd/WwyXHd7L4J9vTI/nn5Ap9AVUgaAE15VvQ9MOGmJJygEUklupqIrFnor/tjTwRU+tQw== monaco-editor@~0.17.0: version "0.17.1" @@ -20490,16 +20429,11 @@ murmurhash3js@3.0.1: resolved "https://registry.yarnpkg.com/murmurhash3js/-/murmurhash3js-3.0.1.tgz#3e983e5b47c2a06f43a713174e7e435ca044b998" integrity sha1-Ppg+W0fCoG9DpxMXTn5DXKBEuZg= -mustache@2.3.2: +mustache@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.2.tgz#a6d4d9c3f91d13359ab889a812954f9230a3d0c5" integrity sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ== -mustache@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.0.tgz#4028f7778b17708a489930a6e52ac3bca0da41d0" - integrity sha1-QCj3d4sXcIpImTCm5SrDvKDaQdA= - mutation-observer@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/mutation-observer/-/mutation-observer-1.0.3.tgz#42e9222b101bca82e5ba9d5a7acf4a14c0f263d0" @@ -20685,7 +20619,7 @@ next-tick@1: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= -ngreact@0.5.1, ngreact@^0.5.1: +ngreact@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/ngreact/-/ngreact-0.5.1.tgz#2dcccc1541771796689d13e51bb8d5010af41c57" integrity sha512-u/jOWS0KF/twS09O+yuBgNNEEytEhrmSfLTewAuglDSfEYru6a4I8tUnU4fs9/WvlRWbvJTk7WEnwbGamM+Kvg== @@ -20753,7 +20687,7 @@ node-environment-flags@1.0.6: object.getownpropertydescriptors "^2.0.3" semver "^5.7.0" -node-fetch@2.1.2, node-fetch@2.6.1, node-fetch@^1.0.1, node-fetch@^2.3.0, node-fetch@^2.6.0, node-fetch@^2.6.1: +node-fetch@2.1.2, node-fetch@^1.0.1, node-fetch@^2.3.0, node-fetch@^2.6.0, node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== @@ -20937,7 +20871,7 @@ node-releases@^1.1.53: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.58.tgz#8ee20eef30fa60e52755fcc0942def5a734fe935" integrity sha512-NxBudgVKiRh/2aPWMgPR7bPTX0VPmGx5QBwCtdHitnqFE5/O8DeBXuIMH1nwNnw/aMo6AjOrpsHzfY3UbUJ7yg== -node-sass@^4.13.0, node-sass@^4.13.1: +node-sass@^4.13.1: version "4.13.1" resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.13.1.tgz#9db5689696bb2eec2c32b98bfea4c7a2e992d0a3" integrity sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw== @@ -21050,11 +20984,6 @@ normalize-package-data@^2.5.0: semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" -normalize-path@3.0.0, normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - normalize-path@^2.0.1, normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" @@ -21062,6 +20991,11 @@ normalize-path@^2.0.1, normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + normalize-range@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" @@ -22593,16 +22527,16 @@ png-js@^1.0.0: resolved "https://registry.yarnpkg.com/png-js/-/png-js-1.0.0.tgz#e5484f1e8156996e383aceebb3789fd75df1874d" integrity sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g== -pngjs@3.4.0, pngjs@^3.3.3, pngjs@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f" - integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== - pngjs@^3.0.0: version "3.3.1" resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.1.tgz#8e14e6679ee7424b544334c3b2d21cea6d8c209a" integrity sha512-ggXCTsqHRIsGMkHlCEhbHhUmNTA2r1lpkE0NL4Q9S8spkXbm4vE9TVmPso2AGYn90Gltdz8W5CyzhcIGg2Gejg== +pngjs@^3.3.3, pngjs@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f" + integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== + pnp-webpack-plugin@1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.5.0.tgz#62a1cd3068f46d564bb33c56eb250e4d586676eb" @@ -22630,16 +22564,16 @@ polished@^3.3.1: dependencies: "@babel/runtime" "^7.4.5" -popper.js@^1.14.1, popper.js@^1.14.7: - version "1.15.0" - resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2" - integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA== - popper.js@^1.14.4: version "1.14.7" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.7.tgz#e31ec06cfac6a97a53280c3e55e4e0c860e7738e" integrity sha512-4q1hNvoUre/8srWsH7hnoSJ5xVmIL4qgz+s4qf2TnJIMyZFUFMGH+9vE7mXynAlHSZ/NdTmmow86muD0myUkVQ== +popper.js@^1.14.7: + version "1.15.0" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2" + integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA== + portfinder@^1.0.26: version "1.0.27" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.27.tgz#a41333c116b5e5f3d380f9745ac2f35084c4c758" @@ -23405,7 +23339,7 @@ raw-body@~1.1.0: bytes "1" string_decoder "0.10" -raw-loader@3.1.0, raw-loader@^3.1.0: +raw-loader@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-3.1.0.tgz#5e9d399a5a222cc0de18f42c3bc5e49677532b3f" integrity sha512-lzUVMuJ06HF4rYveaz9Tv0WRlUMxJ0Y1hgSkkgg+50iEdaI0TthyEDe08KIHb0XsF6rn8WYTqPCaGTZg3sX+qA== @@ -23451,16 +23385,6 @@ re2@^1.15.4: nan "^2.14.1" node-gyp "^7.0.0" -react-ace@^5.5.0: - version "5.10.0" - resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-5.10.0.tgz#e328b37ac52759f700be5afdb86ada2f5ec84c5e" - integrity sha512-aEK/XZCowP8IXq91e2DYqOtGhabk1bbjt+fyeW0UBcIkzDzP/RX/MeJKeyW7wsZcwElACVwyy9nnwXBTqgky3A== - dependencies: - brace "^0.11.0" - lodash.get "^4.4.2" - lodash.isequal "^4.1.1" - prop-types "^15.5.8" - react-ace@^5.9.0: version "5.9.0" resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-5.9.0.tgz#427a1cc4869b960a6f9748aa7eb169a9269fc336" @@ -23545,16 +23469,6 @@ react-color@^2.17.0: reactcss "^1.2.0" tinycolor2 "^1.4.1" -react-datepicker@v1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-1.4.1.tgz#ee171b71d9853e56f9eece5fc3186402f4648683" - integrity sha512-O/ExTWLS81pyWJWLFg1BRQEr9S/BDd6iMEkGctxQmVrRw2srW8DNdnQm5UgFNu8LoSZGMDvI55OghYZvDpWJhw== - dependencies: - classnames "^2.2.5" - prop-types "^15.6.0" - react-onclickoutside "^6.7.1" - react-popper "^0.9.1" - react-datetime@^2.14.0: version "2.15.0" resolved "https://registry.yarnpkg.com/react-datetime/-/react-datetime-2.15.0.tgz#a8f7da6c58b6b45dbeea32d4e8485db17614e12c" @@ -23596,15 +23510,6 @@ react-dev-utils@^9.0.0: strip-ansi "5.2.0" text-table "0.2.0" -react-docgen-typescript-loader@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/react-docgen-typescript-loader/-/react-docgen-typescript-loader-3.1.0.tgz#09cacf872617c97f946ee920d2239f51d543be41" - integrity sha512-gY+b7RkRPty5ZN4NMQ+jwx9MzTVuIj6LJCwdWRAi1+nrHJfH2gMMytQfxFdzQ7BlgD4COWnSE8Ixtl2L62kCRw== - dependencies: - "@webpack-contrib/schema-utils" "^1.0.0-beta.0" - loader-utils "^1.2.3" - react-docgen-typescript "^1.12.3" - react-docgen-typescript-loader@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/react-docgen-typescript-loader/-/react-docgen-typescript-loader-3.1.1.tgz#c1992538524fb9e45246d6c1314ddcfbf26e9d08" @@ -23747,13 +23652,6 @@ react-hotkeys@2.0.0: dependencies: prop-types "^15.6.1" -react-input-autosize@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.1.tgz#ec428fa15b1592994fb5f9aa15bb1eb6baf420f8" - integrity sha512-3+K4CD13iE4lQQ2WlF8PuV5htfmTRLH6MDnfndHM6LuBRszuXnuyIfE7nhSKt8AzRBZ50bu0sAhkNMeS5pxQQA== - dependencies: - prop-types "^15.5.8" - react-input-autosize@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.2.tgz#fcaa7020568ec206bc04be36f4eb68e647c4d8c2" @@ -23853,11 +23751,6 @@ react-onclickoutside@^6.5.0: resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz#6a5b5b8b4eae6b776259712c89c8a2b36b17be93" integrity sha512-p84kBqGaMoa7VYT0vZ/aOYRfJB+gw34yjpda1Z5KeLflg70HipZOT+MXQenEhdkPAABuE2Astq4zEPdMqUQxcg== -react-onclickoutside@^6.7.1: - version "6.8.0" - resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.8.0.tgz#9f91b5b3ed59f4d9e43fd71620dc200773a4d569" - integrity sha512-5Q4Rn7QLEoh7WIe66KFvYIpWJ49GeHoygP1/EtJyZjXKgrWH19Tf0Ty3lWyQzrEEDyLOwUvvmBFSE3dcDdvagA== - react-popper-tooltip@^2.10.1, react-popper-tooltip@^2.8.3: version "2.11.1" resolved "https://registry.yarnpkg.com/react-popper-tooltip/-/react-popper-tooltip-2.11.1.tgz#3c4bdfd8bc10d1c2b9a162e859bab8958f5b2644" @@ -23866,14 +23759,6 @@ react-popper-tooltip@^2.10.1, react-popper-tooltip@^2.8.3: "@babel/runtime" "^7.9.2" react-popper "^1.3.7" -react-popper@^0.9.1: - version "0.9.5" - resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-0.9.5.tgz#02a24ef3eec33af9e54e8358ab70eb0e331edd05" - integrity sha1-AqJO8+7DOvnlToNYq3DrDjMe3QU= - dependencies: - popper.js "^1.14.1" - prop-types "^15.6.1" - react-popper@^1.3.7: version "1.3.7" resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.7.tgz#f6a3471362ef1f0d10a4963673789de1baca2324" @@ -24149,18 +24034,6 @@ react-virtualized-auto-sizer@^1.0.2: resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd" integrity sha512-MYXhTY1BZpdJFjUovvYHVBmkq79szK/k7V3MO+36gJkWGkrXKtyr4vCPtpphaTLRAdDNoYEYFZWE8LjN+PIHNg== -react-virtualized@^9.18.5: - version "9.20.1" - resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.20.1.tgz#02dc08fe9070386b8c48e2ac56bce7af0208d22d" - integrity sha512-xIWxBsyNAjceqD3hsE0nw5TcDVxKbIepsHhvS2XneHmNz0KlKxdLdGBmGZBM9ZesEmbZ5EO0Sw70TB1MeCmpbQ== - dependencies: - babel-runtime "^6.26.0" - classnames "^2.2.3" - dom-helpers "^2.4.0 || ^3.0.0" - loose-envify "^1.3.0" - prop-types "^15.6.0" - react-lifecycles-compat "^3.0.4" - react-virtualized@^9.21.2: version "9.21.2" resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.21.2.tgz#02e6df65c1e020c8dbf574ec4ce971652afca84e" @@ -24584,11 +24457,6 @@ redux-saga@^1.1.3: dependencies: "@redux-saga/core" "^1.1.3" -redux-thunk@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5" - integrity sha1-5hWhbha0ehmlFXZhM9Hj6Zt4UuU= - redux-thunk@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" @@ -25523,13 +25391,6 @@ rxjs-marbles@^5.0.6: dependencies: fast-equals "^2.0.0" -rxjs@6.5.5, rxjs@^6.5.5: - version "6.5.5" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec" - integrity sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ== - dependencies: - tslib "^1.9.0" - rxjs@^5.5.2: version "5.5.12" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.12.tgz#6fa61b8a77c3d793dbaf270bee2f43f652d741cc" @@ -25544,6 +25405,13 @@ rxjs@^6.1.0, rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.1: dependencies: tslib "^1.9.0" +rxjs@^6.5.5: + version "6.5.5" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec" + integrity sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ== + dependencies: + tslib "^1.9.0" + rxjs@^6.6.0: version "6.6.0" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.0.tgz#af2901eedf02e3a83ffa7f886240ff9018bbec84" @@ -25835,11 +25703,6 @@ semver@5.5.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA== -semver@5.7.0: - version "5.7.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" - integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== - semver@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" @@ -26180,13 +26043,6 @@ simple-git@1.116.0: dependencies: debug "^4.0.1" -simple-git@^1.91.0: - version "1.92.0" - resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-1.92.0.tgz#6061468eb7d19f0141078fc742e62457e910f547" - integrity sha1-YGFGjrfRnwFBB4/HQuYkV+kQ9Uc= - dependencies: - debug "^3.1.0" - simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" @@ -27469,7 +27325,7 @@ symbol.prototype.description@^1.0.0: dependencies: has-symbols "^1.0.0" -tabbable@1.1.3, tabbable@^1.1.0: +tabbable@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-1.1.3.tgz#0e4ee376f3631e42d7977a074dbd2b3827843081" integrity sha512-nOWwx35/JuDI4ONuF0ZTo6lYvI0fY0tZCH1ErzY2EXfu4az50ZyiUX8X073FLiZtmWUVlkRnuXsehjJgCw9tYg== @@ -28004,13 +27860,6 @@ tmp@0.0.30: dependencies: os-tmpdir "~1.0.1" -tmp@0.1.0, tmp@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877" - integrity sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw== - dependencies: - rimraf "^2.6.3" - tmp@^0.0.29: version "0.0.29" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.29.tgz#f25125ff0dd9da3ccb0c2dd371ee1288bb9128c0" @@ -29079,7 +28928,7 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= -url-loader@2.2.0, url-loader@^2.0.1: +url-loader@^2.0.1: version "2.2.0" resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-2.2.0.tgz#af321aece1fd0d683adc8aaeb27829f29c75b46e" integrity sha512-G8nk3np8ZAnwhHXas1JxJEwJyQdqFXAKJehfgZ/XrC48volFBRtO+FIKtF2u0Ma3bw+4vnDVjHPAQYlF9p2vsw== @@ -30959,7 +30808,7 @@ yargs@~3.10.0: decamelize "^1.0.0" window-size "0.1.0" -yauzl@2.10.0, yauzl@^2.10.0: +yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=