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/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/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 10083ff07fccd..9b1735f6f7e5e 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,7 @@ "@hapi/good-squeeze": "5.2.1", "@hapi/wreck": "^15.0.2", "@kbn/analytics": "1.0.0", + "@kbn/apm-config-loader": "1.0.0", "@kbn/babel-preset": "1.0.0", "@kbn/config": "1.0.0", "@kbn/config-schema": "1.0.0", @@ -154,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", @@ -183,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", @@ -211,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", @@ -222,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", @@ -278,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", @@ -311,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", @@ -392,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", @@ -438,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-apm-config-loader/README.md b/packages/kbn-apm-config-loader/README.md new file mode 100644 index 0000000000000..51623dc745f2c --- /dev/null +++ b/packages/kbn-apm-config-loader/README.md @@ -0,0 +1,13 @@ +# @kbn/apm-config-loader + +Configuration loader for the APM instrumentation script. + +This module is only meant to be used by the APM instrumentation script (`src/apm.js`) +to load the required configuration options from the `kibana.yaml` configuration file with +default values. + +### Why not just use @kbn-config? + +`@kbn/config` is the recommended way to load and read the kibana configuration file, +however in the specific case of APM, we want to only need the minimal dependencies +before loading `elastic-apm-node` to avoid losing instrumentation on the already loaded modules. \ No newline at end of file diff --git a/packages/kbn-apm-config-loader/__fixtures__/config.yml b/packages/kbn-apm-config-loader/__fixtures__/config.yml new file mode 100644 index 0000000000000..b0706d8ff8ea0 --- /dev/null +++ b/packages/kbn-apm-config-loader/__fixtures__/config.yml @@ -0,0 +1,11 @@ +pid: + enabled: true + file: '/var/run/kibana.pid' + obj: { val: 3 } + arr: [1] + empty_obj: {} + empty_arr: [] +obj: { val: 3 } +arr: [1, 2] +empty_obj: {} +empty_arr: [] diff --git a/packages/kbn-apm-config-loader/__fixtures__/config_flat.yml b/packages/kbn-apm-config-loader/__fixtures__/config_flat.yml new file mode 100644 index 0000000000000..a687a9a9088bf --- /dev/null +++ b/packages/kbn-apm-config-loader/__fixtures__/config_flat.yml @@ -0,0 +1,6 @@ +pid.enabled: true +pid.file: '/var/run/kibana.pid' +pid.obj: { val: 3 } +pid.arr: [1, 2] +pid.empty_obj: {} +pid.empty_arr: [] diff --git a/packages/kbn-apm-config-loader/__fixtures__/en_var_ref_config.yml b/packages/kbn-apm-config-loader/__fixtures__/en_var_ref_config.yml new file mode 100644 index 0000000000000..761f6a43ba452 --- /dev/null +++ b/packages/kbn-apm-config-loader/__fixtures__/en_var_ref_config.yml @@ -0,0 +1,5 @@ +foo: 1 +bar: "pre-${KBN_ENV_VAR1}-mid-${KBN_ENV_VAR2}-post" + +elasticsearch: + requestHeadersWhitelist: ["${KBN_ENV_VAR1}", "${KBN_ENV_VAR2}"] diff --git a/packages/kbn-apm-config-loader/__fixtures__/one.yml b/packages/kbn-apm-config-loader/__fixtures__/one.yml new file mode 100644 index 0000000000000..ccef51b546194 --- /dev/null +++ b/packages/kbn-apm-config-loader/__fixtures__/one.yml @@ -0,0 +1,9 @@ +foo: 1 +bar: true +xyz: ['1', '2'] +empty_arr: [] +abc: + def: test + qwe: 1 + zyx: { val: 1 } +pom.bom: 3 diff --git a/packages/kbn-apm-config-loader/__fixtures__/two.yml b/packages/kbn-apm-config-loader/__fixtures__/two.yml new file mode 100644 index 0000000000000..a2ec41265d50f --- /dev/null +++ b/packages/kbn-apm-config-loader/__fixtures__/two.yml @@ -0,0 +1,10 @@ +foo: 2 +baz: bonkers +xyz: ['3', '4'] +arr: [1] +empty_arr: [] +abc: + ghi: test2 + qwe: 2 + zyx: {} +pom.mob: 4 diff --git a/packages/kbn-apm-config-loader/package.json b/packages/kbn-apm-config-loader/package.json new file mode 100644 index 0000000000000..1982ccdeda0ff --- /dev/null +++ b/packages/kbn-apm-config-loader/package.json @@ -0,0 +1,23 @@ +{ + "name": "@kbn/apm-config-loader", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "version": "1.0.0", + "license": "Apache-2.0", + "private": true, + "scripts": { + "build": "tsc", + "kbn:bootstrap": "yarn build", + "kbn:watch": "yarn build --watch" + }, + "dependencies": { + "@elastic/safer-lodash-set": "0.0.0", + "@kbn/utils": "1.0.0", + "js-yaml": "3.13.1", + "lodash": "^4.17.20" + }, + "devDependencies": { + "typescript": "4.0.2", + "tsd": "^0.7.4" + } +} diff --git a/packages/kbn-apm-config-loader/src/config.test.mocks.ts b/packages/kbn-apm-config-loader/src/config.test.mocks.ts new file mode 100644 index 0000000000000..a0422665a55d2 --- /dev/null +++ b/packages/kbn-apm-config-loader/src/config.test.mocks.ts @@ -0,0 +1,66 @@ +/* + * 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 { join } from 'path'; +const childProcessModule = jest.requireActual('child_process'); +const fsModule = jest.requireActual('fs'); + +export const mockedRootDir = '/root'; + +export const packageMock = { + raw: {} as any, +}; +jest.doMock(join(mockedRootDir, 'package.json'), () => packageMock.raw, { virtual: true }); + +export const devConfigMock = { + raw: {} as any, +}; +jest.doMock(join(mockedRootDir, 'config', 'apm.dev.js'), () => devConfigMock.raw, { + virtual: true, +}); + +export const gitRevExecMock = jest.fn(); +jest.doMock('child_process', () => ({ + ...childProcessModule, + execSync: (command: string, options: any) => { + if (command.startsWith('git rev-parse')) { + return gitRevExecMock(command, options); + } + return childProcessModule.execSync(command, options); + }, +})); + +export const readUuidFileMock = jest.fn(); +jest.doMock('fs', () => ({ + ...fsModule, + readFileSync: (path: string, options: any) => { + if (path.endsWith('uuid')) { + return readUuidFileMock(path, options); + } + return fsModule.readFileSync(path, options); + }, +})); + +export const resetAllMocks = () => { + packageMock.raw = {}; + devConfigMock.raw = {}; + gitRevExecMock.mockReset(); + readUuidFileMock.mockReset(); + jest.resetModules(); +}; diff --git a/packages/kbn-apm-config-loader/src/config.test.ts b/packages/kbn-apm-config-loader/src/config.test.ts new file mode 100644 index 0000000000000..fe6247673e312 --- /dev/null +++ b/packages/kbn-apm-config-loader/src/config.test.ts @@ -0,0 +1,158 @@ +/* + * 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 { + packageMock, + mockedRootDir, + gitRevExecMock, + devConfigMock, + readUuidFileMock, + resetAllMocks, +} from './config.test.mocks'; + +import { ApmConfiguration } from './config'; + +describe('ApmConfiguration', () => { + beforeEach(() => { + packageMock.raw = { + version: '8.0.0', + build: { + sha: 'sha', + }, + }; + }); + + afterEach(() => { + resetAllMocks(); + }); + + it('sets the correct service name', () => { + packageMock.raw = { + version: '9.2.1', + }; + const config = new ApmConfiguration(mockedRootDir, {}, false); + expect(config.getConfig('myservice').serviceName).toBe('myservice-9_2_1'); + }); + + it('sets the git revision from `git rev-parse` command in non distribution mode', () => { + gitRevExecMock.mockReturnValue('some-git-rev'); + const config = new ApmConfiguration(mockedRootDir, {}, false); + expect(config.getConfig('serviceName').globalLabels.git_rev).toBe('some-git-rev'); + }); + + it('sets the git revision from `pkg.build.sha` in distribution mode', () => { + gitRevExecMock.mockReturnValue('dev-sha'); + packageMock.raw = { + version: '9.2.1', + build: { + sha: 'distribution-sha', + }, + }; + const config = new ApmConfiguration(mockedRootDir, {}, true); + expect(config.getConfig('serviceName').globalLabels.git_rev).toBe('distribution-sha'); + }); + + it('reads the kibana uuid from the uuid file', () => { + readUuidFileMock.mockReturnValue('instance-uuid'); + const config = new ApmConfiguration(mockedRootDir, {}, false); + expect(config.getConfig('serviceName').globalLabels.kibana_uuid).toBe('instance-uuid'); + }); + + it('uses the uuid from the kibana config if present', () => { + readUuidFileMock.mockReturnValue('uuid-from-file'); + const kibanaConfig = { + server: { + uuid: 'uuid-from-config', + }, + }; + const config = new ApmConfiguration(mockedRootDir, kibanaConfig, false); + expect(config.getConfig('serviceName').globalLabels.kibana_uuid).toBe('uuid-from-config'); + }); + + it('uses the correct default config depending on the `isDistributable` parameter', () => { + let config = new ApmConfiguration(mockedRootDir, {}, false); + expect(config.getConfig('serviceName')).toEqual( + expect.objectContaining({ + serverUrl: expect.any(String), + secretToken: expect.any(String), + }) + ); + + config = new ApmConfiguration(mockedRootDir, {}, true); + expect(Object.keys(config.getConfig('serviceName'))).not.toContain('serverUrl'); + }); + + it('loads the configuration from the kibana config file', () => { + const kibanaConfig = { + elastic: { + apm: { + active: true, + serverUrl: 'https://url', + secretToken: 'secret', + }, + }, + }; + const config = new ApmConfiguration(mockedRootDir, kibanaConfig, true); + expect(config.getConfig('serviceName')).toEqual( + expect.objectContaining({ + active: true, + serverUrl: 'https://url', + secretToken: 'secret', + }) + ); + }); + + it('loads the configuration from the dev config is present', () => { + devConfigMock.raw = { + active: true, + serverUrl: 'https://dev-url.co', + }; + const config = new ApmConfiguration(mockedRootDir, {}, true); + expect(config.getConfig('serviceName')).toEqual( + expect.objectContaining({ + active: true, + serverUrl: 'https://dev-url.co', + }) + ); + }); + + it('respect the precedence of the dev config', () => { + const kibanaConfig = { + elastic: { + apm: { + active: true, + serverUrl: 'https://url', + secretToken: 'secret', + }, + }, + }; + devConfigMock.raw = { + active: true, + serverUrl: 'https://dev-url.co', + }; + const config = new ApmConfiguration(mockedRootDir, kibanaConfig, true); + expect(config.getConfig('serviceName')).toEqual( + expect.objectContaining({ + active: true, + serverUrl: 'https://dev-url.co', + secretToken: 'secret', + }) + ); + }); +}); diff --git a/packages/kbn-apm-config-loader/src/config.ts b/packages/kbn-apm-config-loader/src/config.ts new file mode 100644 index 0000000000000..aab82c6c06a58 --- /dev/null +++ b/packages/kbn-apm-config-loader/src/config.ts @@ -0,0 +1,139 @@ +/* + * 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 { join } from 'path'; +import { merge, get } from 'lodash'; +import { execSync } from 'child_process'; +// deep import to avoid loading the whole package +import { getDataPath } from '@kbn/utils/target/path'; +import { readFileSync } from 'fs'; +import { ApmAgentConfig } from './types'; + +const getDefaultConfig = (isDistributable: boolean): ApmAgentConfig => { + if (isDistributable) { + return { + active: false, + globalLabels: {}, + }; + } + return { + active: false, + serverUrl: 'https://f1542b814f674090afd914960583265f.apm.us-central1.gcp.cloud.es.io:443', + // The secretToken below is intended to be hardcoded in this file even though + // it makes it public. This is not a security/privacy issue. Normally we'd + // instead disable the need for a secretToken in the APM Server config where + // the data is transmitted to, but due to how it's being hosted, it's easier, + // for now, to simply leave it in. + secretToken: 'R0Gjg46pE9K9wGestd', + globalLabels: {}, + breakdownMetrics: true, + centralConfig: false, + logUncaughtExceptions: true, + }; +}; + +export class ApmConfiguration { + private baseConfig?: any; + private kibanaVersion: string; + private pkgBuild: Record; + + constructor( + private readonly rootDir: string, + private readonly rawKibanaConfig: Record, + private readonly isDistributable: boolean + ) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { version, build } = require(join(this.rootDir, 'package.json')); + this.kibanaVersion = version.replace(/\./g, '_'); + this.pkgBuild = build; + } + + public getConfig(serviceName: string): ApmAgentConfig { + return { + ...this.getBaseConfig(), + serviceName: `${serviceName}-${this.kibanaVersion}`, + }; + } + + private getBaseConfig() { + if (!this.baseConfig) { + const apmConfig = merge( + getDefaultConfig(this.isDistributable), + this.getConfigFromKibanaConfig(), + this.getDevConfig() + ); + + const rev = this.getGitRev(); + if (rev !== null) { + apmConfig.globalLabels.git_rev = rev; + } + + const uuid = this.getKibanaUuid(); + if (uuid) { + apmConfig.globalLabels.kibana_uuid = uuid; + } + this.baseConfig = apmConfig; + } + + return this.baseConfig; + } + + private getConfigFromKibanaConfig(): ApmAgentConfig { + return get(this.rawKibanaConfig, 'elastic.apm', {}); + } + + private getKibanaUuid() { + // try to access the `server.uuid` value from the config file first. + // if not manually defined, we will then read the value from the `{DATA_FOLDER}/uuid` file. + // note that as the file is created by the platform AFTER apm init, the file + // will not be present at first startup, but there is nothing we can really do about that. + if (get(this.rawKibanaConfig, 'server.uuid')) { + return this.rawKibanaConfig.server.uuid; + } + + const dataPath: string = get(this.rawKibanaConfig, 'path.data') || getDataPath(); + try { + const filename = join(dataPath, 'uuid'); + return readFileSync(filename, 'utf-8'); + } catch (e) {} // eslint-disable-line no-empty + } + + private getDevConfig(): ApmAgentConfig { + try { + const apmDevConfigPath = join(this.rootDir, 'config', 'apm.dev.js'); + return require(apmDevConfigPath); + } catch (e) { + return {}; + } + } + + private getGitRev() { + if (this.isDistributable) { + return this.pkgBuild.sha; + } + try { + return execSync('git rev-parse --short HEAD', { + encoding: 'utf-8' as BufferEncoding, + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + } catch (e) { + return null; + } + } +} diff --git a/packages/kbn-apm-config-loader/src/config_loader.test.mocks.ts b/packages/kbn-apm-config-loader/src/config_loader.test.mocks.ts new file mode 100644 index 0000000000000..74b50d9daf632 --- /dev/null +++ b/packages/kbn-apm-config-loader/src/config_loader.test.mocks.ts @@ -0,0 +1,45 @@ +/* + * 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. + */ + +export const getConfigurationFilePathsMock = jest.fn(); +jest.doMock('./utils/get_config_file_paths', () => ({ + getConfigurationFilePaths: getConfigurationFilePathsMock, +})); + +export const getConfigFromFilesMock = jest.fn(); +jest.doMock('./utils/read_config', () => ({ + getConfigFromFiles: getConfigFromFilesMock, +})); + +export const applyConfigOverridesMock = jest.fn(); +jest.doMock('./utils/apply_config_overrides', () => ({ + applyConfigOverrides: applyConfigOverridesMock, +})); + +export const ApmConfigurationMock = jest.fn(); +jest.doMock('./config', () => ({ + ApmConfiguration: ApmConfigurationMock, +})); + +export const resetAllMocks = () => { + getConfigurationFilePathsMock.mockReset(); + getConfigFromFilesMock.mockReset(); + applyConfigOverridesMock.mockReset(); + ApmConfigurationMock.mockReset(); +}; diff --git a/packages/kbn-apm-config-loader/src/config_loader.test.ts b/packages/kbn-apm-config-loader/src/config_loader.test.ts new file mode 100644 index 0000000000000..da59237de231e --- /dev/null +++ b/packages/kbn-apm-config-loader/src/config_loader.test.ts @@ -0,0 +1,75 @@ +/* + * 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 { + ApmConfigurationMock, + applyConfigOverridesMock, + getConfigFromFilesMock, + getConfigurationFilePathsMock, + resetAllMocks, +} from './config_loader.test.mocks'; + +import { loadConfiguration } from './config_loader'; + +describe('loadConfiguration', () => { + const argv = ['some', 'arbitrary', 'args']; + const rootDir = '/root/dir'; + const isDistributable = false; + + afterEach(() => { + resetAllMocks(); + }); + + it('calls `getConfigurationFilePaths` with the correct arguments', () => { + loadConfiguration(argv, rootDir, isDistributable); + expect(getConfigurationFilePathsMock).toHaveBeenCalledTimes(1); + expect(getConfigurationFilePathsMock).toHaveBeenCalledWith(argv); + }); + + it('calls `getConfigFromFiles` with the correct arguments', () => { + const configPaths = ['/path/to/config', '/path/to/other/config']; + getConfigurationFilePathsMock.mockReturnValue(configPaths); + + loadConfiguration(argv, rootDir, isDistributable); + expect(getConfigFromFilesMock).toHaveBeenCalledTimes(1); + expect(getConfigFromFilesMock).toHaveBeenCalledWith(configPaths); + }); + + it('calls `applyConfigOverrides` with the correct arguments', () => { + const config = { server: { uuid: 'uuid' } }; + getConfigFromFilesMock.mockReturnValue(config); + + loadConfiguration(argv, rootDir, isDistributable); + expect(applyConfigOverridesMock).toHaveBeenCalledTimes(1); + expect(applyConfigOverridesMock).toHaveBeenCalledWith(config, argv); + }); + + it('creates and return an `ApmConfiguration` instance', () => { + const apmInstance = { apmInstance: true }; + ApmConfigurationMock.mockImplementation(() => apmInstance); + + const config = { server: { uuid: 'uuid' } }; + getConfigFromFilesMock.mockReturnValue(config); + + const instance = loadConfiguration(argv, rootDir, isDistributable); + expect(ApmConfigurationMock).toHaveBeenCalledTimes(1); + expect(ApmConfigurationMock).toHaveBeenCalledWith(rootDir, config, isDistributable); + expect(instance).toBe(apmInstance); + }); +}); diff --git a/packages/kbn-apm-config-loader/src/config_loader.ts b/packages/kbn-apm-config-loader/src/config_loader.ts new file mode 100644 index 0000000000000..edddd445b9b7a --- /dev/null +++ b/packages/kbn-apm-config-loader/src/config_loader.ts @@ -0,0 +1,39 @@ +/* + * 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 { getConfigurationFilePaths, getConfigFromFiles, applyConfigOverrides } from './utils'; +import { ApmConfiguration } from './config'; + +/** + * Load the APM configuration. + * + * @param argv the `process.argv` arguments + * @param rootDir The root directory of kibana (where the sources and the `package.json` file are) + * @param production true for production builds, false otherwise + */ +export const loadConfiguration = ( + argv: string[], + rootDir: string, + isDistributable: boolean +): ApmConfiguration => { + const configPaths = getConfigurationFilePaths(argv); + const rawConfiguration = getConfigFromFiles(configPaths); + applyConfigOverrides(rawConfiguration, argv); + return new ApmConfiguration(rootDir, rawConfiguration, isDistributable); +}; diff --git a/examples/alerting_example/public/index.ts b/packages/kbn-apm-config-loader/src/index.ts similarity index 84% rename from examples/alerting_example/public/index.ts rename to packages/kbn-apm-config-loader/src/index.ts index 4a2bfc79903c3..0d9c057c7cf89 100644 --- a/examples/alerting_example/public/index.ts +++ b/packages/kbn-apm-config-loader/src/index.ts @@ -17,6 +17,6 @@ * under the License. */ -import { AlertingExamplePlugin } from './plugin'; - -export const plugin = () => new AlertingExamplePlugin(); +export { loadConfiguration } from './config_loader'; +export type { ApmConfiguration } from './config'; +export type { ApmAgentConfig } from './types'; diff --git a/packages/kbn-apm-config-loader/src/types.ts b/packages/kbn-apm-config-loader/src/types.ts new file mode 100644 index 0000000000000..172edfe0af009 --- /dev/null +++ b/packages/kbn-apm-config-loader/src/types.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +// There is an (incomplete) `AgentConfigOptions` type declared in node_modules/elastic-apm-node/index.d.ts +// but it's not exported, and using ts tricks to retrieve the type via Parameters[0] +// causes errors in the generated .d.ts file because of esModuleInterop and the fact that the apm module +// is just exporting an instance of the `ApmAgent` type. +export type ApmAgentConfig = Record; diff --git a/packages/kbn-apm-config-loader/src/utils/__snapshots__/read_config.test.ts.snap b/packages/kbn-apm-config-loader/src/utils/__snapshots__/read_config.test.ts.snap new file mode 100644 index 0000000000000..afdce4e76d3f5 --- /dev/null +++ b/packages/kbn-apm-config-loader/src/utils/__snapshots__/read_config.test.ts.snap @@ -0,0 +1,108 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`different cwd() resolves relative files based on the cwd 1`] = ` +Object { + "abc": Object { + "def": "test", + "qwe": 1, + "zyx": Object { + "val": 1, + }, + }, + "bar": true, + "empty_arr": Array [], + "foo": 1, + "pom": Object { + "bom": 3, + }, + "xyz": Array [ + "1", + "2", + ], +} +`; + +exports[`reads and merges multiple yaml files from file system and parses to json 1`] = ` +Object { + "abc": Object { + "def": "test", + "ghi": "test2", + "qwe": 2, + "zyx": Object {}, + }, + "arr": Array [ + 1, + ], + "bar": true, + "baz": "bonkers", + "empty_arr": Array [], + "foo": 2, + "pom": Object { + "bom": 3, + "mob": 4, + }, + "xyz": Array [ + "3", + "4", + ], +} +`; + +exports[`reads single yaml from file system and parses to json 1`] = ` +Object { + "arr": Array [ + 1, + 2, + ], + "empty_arr": Array [], + "empty_obj": Object {}, + "obj": Object { + "val": 3, + }, + "pid": Object { + "arr": Array [ + 1, + ], + "empty_arr": Array [], + "empty_obj": Object {}, + "enabled": true, + "file": "/var/run/kibana.pid", + "obj": Object { + "val": 3, + }, + }, +} +`; + +exports[`returns a deep object 1`] = ` +Object { + "pid": Object { + "arr": Array [ + 1, + 2, + ], + "empty_arr": Array [], + "empty_obj": Object {}, + "enabled": true, + "file": "/var/run/kibana.pid", + "obj": Object { + "val": 3, + }, + }, +} +`; + +exports[`should inject an environment variable value when setting a value with \${ENV_VAR} 1`] = ` +Object { + "bar": "pre-val1-mid-val2-post", + "elasticsearch": Object { + "requestHeadersWhitelist": Array [ + "val1", + "val2", + ], + }, + "foo": 1, +} +`; + +exports[`should throw an exception when referenced environment variable in a config value does not exist 1`] = `"Unknown environment variable referenced in config : KBN_ENV_VAR1"`; diff --git a/packages/kbn-apm-config-loader/src/utils/apply_config_overrides.test.ts b/packages/kbn-apm-config-loader/src/utils/apply_config_overrides.test.ts new file mode 100644 index 0000000000000..1d86f7e1f6e8a --- /dev/null +++ b/packages/kbn-apm-config-loader/src/utils/apply_config_overrides.test.ts @@ -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 { applyConfigOverrides } from './apply_config_overrides'; + +describe('applyConfigOverrides', () => { + it('overrides `server.uuid` when provided as a command line argument', () => { + const config: Record = { + server: { + uuid: 'from-config', + }, + }; + const argv = ['--server.uuid', 'from-argv']; + + applyConfigOverrides(config, argv); + + expect(config.server.uuid).toEqual('from-argv'); + }); + + it('overrides `path.data` when provided as a command line argument', () => { + const config: Record = { + path: { + data: '/from/config', + }, + }; + const argv = ['--path.data', '/from/argv']; + + applyConfigOverrides(config, argv); + + expect(config.path.data).toEqual('/from/argv'); + }); + + it('properly set the overridden properties even if the parent object is not present in the config', () => { + const config: Record = {}; + const argv = ['--server.uuid', 'from-argv', '--path.data', '/data-path']; + + applyConfigOverrides(config, argv); + + expect(config.server.uuid).toEqual('from-argv'); + expect(config.path.data).toEqual('/data-path'); + }); +}); diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/update_ingest_pipeline.js b/packages/kbn-apm-config-loader/src/utils/apply_config_overrides.ts similarity index 58% rename from src/dev/code_coverage/ingest_coverage/team_assignment/update_ingest_pipeline.js rename to packages/kbn-apm-config-loader/src/utils/apply_config_overrides.ts index 22a9d0a461ebf..6a3bf95f9954d 100644 --- a/src/dev/code_coverage/ingest_coverage/team_assignment/update_ingest_pipeline.js +++ b/packages/kbn-apm-config-loader/src/utils/apply_config_overrides.ts @@ -17,21 +17,22 @@ * under the License. */ -import { createFailError } from '@kbn/dev-utils'; -import { ES_HOST } from '../constants'; -import { pretty, green } from '../utils'; +import { set } from '@elastic/safer-lodash-set'; +import { getArgValue } from './read_argv'; -const { Client } = require('@elastic/elasticsearch'); - -const node = ES_HOST; -const client = new Client({ node }); - -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)}`); +/** + * Manually applies the specific configuration overrides we need to load the APM config. + * Currently, only these are needed: + * - server.uuid + * - path.data + */ +export const applyConfigOverrides = (config: Record, argv: string[]) => { + const serverUuid = getArgValue(argv, '--server.uuid'); + if (serverUuid) { + set(config, 'server.uuid', serverUuid); + } + const dataPath = getArgValue(argv, '--path.data'); + if (dataPath) { + set(config, 'path.data', dataPath); } }; diff --git a/packages/kbn-apm-config-loader/src/utils/ensure_deep_object.test.ts b/packages/kbn-apm-config-loader/src/utils/ensure_deep_object.test.ts new file mode 100644 index 0000000000000..5a520fbeef316 --- /dev/null +++ b/packages/kbn-apm-config-loader/src/utils/ensure_deep_object.test.ts @@ -0,0 +1,156 @@ +/* + * 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 { ensureDeepObject } from './ensure_deep_object'; + +test('flat object', () => { + const obj = { + 'foo.a': 1, + 'foo.b': 2, + }; + + expect(ensureDeepObject(obj)).toEqual({ + foo: { + a: 1, + b: 2, + }, + }); +}); + +test('deep object', () => { + const obj = { + foo: { + a: 1, + b: 2, + }, + }; + + expect(ensureDeepObject(obj)).toEqual({ + foo: { + a: 1, + b: 2, + }, + }); +}); + +test('flat within deep object', () => { + const obj = { + foo: { + b: 2, + 'bar.a': 1, + }, + }; + + expect(ensureDeepObject(obj)).toEqual({ + foo: { + b: 2, + bar: { + a: 1, + }, + }, + }); +}); + +test('flat then flat object', () => { + const obj = { + 'foo.bar': { + b: 2, + 'quux.a': 1, + }, + }; + + expect(ensureDeepObject(obj)).toEqual({ + foo: { + bar: { + b: 2, + quux: { + a: 1, + }, + }, + }, + }); +}); + +test('full with empty array', () => { + const obj = { + a: 1, + b: [], + }; + + expect(ensureDeepObject(obj)).toEqual({ + a: 1, + b: [], + }); +}); + +test('full with array of primitive values', () => { + const obj = { + a: 1, + b: [1, 2, 3], + }; + + expect(ensureDeepObject(obj)).toEqual({ + a: 1, + b: [1, 2, 3], + }); +}); + +test('full with array of full objects', () => { + const obj = { + a: 1, + b: [{ c: 2 }, { d: 3 }], + }; + + expect(ensureDeepObject(obj)).toEqual({ + a: 1, + b: [{ c: 2 }, { d: 3 }], + }); +}); + +test('full with array of flat objects', () => { + const obj = { + a: 1, + b: [{ 'c.d': 2 }, { 'e.f': 3 }], + }; + + expect(ensureDeepObject(obj)).toEqual({ + a: 1, + b: [{ c: { d: 2 } }, { e: { f: 3 } }], + }); +}); + +test('flat with flat and array of flat objects', () => { + const obj = { + a: 1, + 'b.c': 2, + d: [3, { 'e.f': 4 }, { 'g.h': 5 }], + }; + + expect(ensureDeepObject(obj)).toEqual({ + a: 1, + b: { c: 2 }, + d: [3, { e: { f: 4 } }, { g: { h: 5 } }], + }); +}); + +test('array composed of flat objects', () => { + const arr = [{ 'c.d': 2 }, { 'e.f': 3 }]; + + expect(ensureDeepObject(arr)).toEqual([{ c: { d: 2 } }, { e: { f: 3 } }]); +}); diff --git a/packages/kbn-apm-config-loader/src/utils/ensure_deep_object.ts b/packages/kbn-apm-config-loader/src/utils/ensure_deep_object.ts new file mode 100644 index 0000000000000..6eaaef983355c --- /dev/null +++ b/packages/kbn-apm-config-loader/src/utils/ensure_deep_object.ts @@ -0,0 +1,61 @@ +/* + * 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 separator = '.'; + +/** + * Recursively traverses through the object's properties and expands ones with + * dot-separated names into nested objects (eg. { a.b: 'c'} -> { a: { b: 'c' }). + * @param obj Object to traverse through. + * @returns Same object instance with expanded properties. + */ +export function ensureDeepObject(obj: any): any { + if (obj == null || typeof obj !== 'object') { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map((item) => ensureDeepObject(item)); + } + + return Object.keys(obj).reduce((fullObject, propertyKey) => { + const propertyValue = obj[propertyKey]; + if (!propertyKey.includes(separator)) { + fullObject[propertyKey] = ensureDeepObject(propertyValue); + } else { + walk(fullObject, propertyKey.split(separator), propertyValue); + } + + return fullObject; + }, {} as any); +} + +function walk(obj: any, keys: string[], value: any) { + const key = keys.shift()!; + if (keys.length === 0) { + obj[key] = value; + return; + } + + if (obj[key] === undefined) { + obj[key] = {}; + } + + walk(obj[key], keys, ensureDeepObject(value)); +} diff --git a/examples/alerting_example/public/alert_types/index.ts b/packages/kbn-apm-config-loader/src/utils/get_config_file_paths.test.ts similarity index 55% rename from examples/alerting_example/public/alert_types/index.ts rename to packages/kbn-apm-config-loader/src/utils/get_config_file_paths.test.ts index db9f855b573e8..c18069f21180b 100644 --- a/examples/alerting_example/public/alert_types/index.ts +++ b/packages/kbn-apm-config-loader/src/utils/get_config_file_paths.test.ts @@ -17,17 +17,23 @@ * 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'; +import { resolve, join } from 'path'; +import { getConfigPath } from '@kbn/utils'; +import { getConfigurationFilePaths } from './get_config_file_paths'; -export function registerNavigation(alerts: AlertingSetup) { - // register default navigation - alerts.registerDefaultNavigation( - ALERTING_EXAMPLE_APP_ID, - (alert: SanitizedAlert) => `/alert/${alert.id}` - ); +describe('getConfigurationFilePaths', () => { + const cwd = process.cwd(); - registerPeopleInSpaceNavigation(alerts); -} + it('retrieve the config file paths from the command line arguments', () => { + const argv = ['--config', './relative-path', '-c', '/absolute-path']; + + expect(getConfigurationFilePaths(argv)).toEqual([ + resolve(cwd, join('.', 'relative-path')), + '/absolute-path', + ]); + }); + + it('fallbacks to `getConfigPath` value', () => { + expect(getConfigurationFilePaths([])).toEqual([getConfigPath()]); + }); +}); diff --git a/packages/kbn-apm-config-loader/src/utils/get_config_file_paths.ts b/packages/kbn-apm-config-loader/src/utils/get_config_file_paths.ts new file mode 100644 index 0000000000000..262f0d1c8b3f5 --- /dev/null +++ b/packages/kbn-apm-config-loader/src/utils/get_config_file_paths.ts @@ -0,0 +1,37 @@ +/* + * 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'; +// deep import to avoid loading the whole package +import { getConfigPath } from '@kbn/utils/target/path'; +import { getArgValues } from './read_argv'; + +/** + * Return the configuration files that needs to be loaded. + * + * This mimics the behavior of the `src/cli/serve/serve.js` cli script by reading + * `-c` and `--config` options from process.argv, and fallbacks to `@kbn/utils`'s `getConfigPath()` + */ +export const getConfigurationFilePaths = (argv: string[]): string[] => { + const rawPaths = getArgValues(argv, ['-c', '--config']); + if (rawPaths.length) { + return rawPaths.map((path) => resolve(process.cwd(), path)); + } + return [getConfigPath()]; +}; diff --git a/examples/alerting_example/server/index.ts b/packages/kbn-apm-config-loader/src/utils/index.ts similarity index 80% rename from examples/alerting_example/server/index.ts rename to packages/kbn-apm-config-loader/src/utils/index.ts index 32e9b181ebb54..03a44e31a44d5 100644 --- a/examples/alerting_example/server/index.ts +++ b/packages/kbn-apm-config-loader/src/utils/index.ts @@ -17,7 +17,6 @@ * under the License. */ -import { PluginInitializer } from 'kibana/server'; -import { AlertingExamplePlugin } from './plugin'; - -export const plugin: PluginInitializer = () => new AlertingExamplePlugin(); +export { getConfigFromFiles } from './read_config'; +export { getConfigurationFilePaths } from './get_config_file_paths'; +export { applyConfigOverrides } from './apply_config_overrides'; diff --git a/packages/kbn-apm-config-loader/src/utils/read_argv.test.ts b/packages/kbn-apm-config-loader/src/utils/read_argv.test.ts new file mode 100644 index 0000000000000..282810e71681e --- /dev/null +++ b/packages/kbn-apm-config-loader/src/utils/read_argv.test.ts @@ -0,0 +1,80 @@ +/* + * 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 { getArgValue, getArgValues } from './read_argv'; + +describe('getArgValues', () => { + it('retrieve the arg value from the provided argv arguments', () => { + const argValues = getArgValues( + ['--config', 'my-config', '--foo', '-b', 'bar', '--config', 'other-config', '--baz'], + '--config' + ); + expect(argValues).toEqual(['my-config', 'other-config']); + }); + + it('accept aliases', () => { + const argValues = getArgValues( + ['--config', 'my-config', '--foo', '-b', 'bar', '-c', 'other-config', '--baz'], + ['--config', '-c'] + ); + expect(argValues).toEqual(['my-config', 'other-config']); + }); + + it('returns an empty array when the arg is not found', () => { + const argValues = getArgValues( + ['--config', 'my-config', '--foo', '-b', 'bar', '-c', 'other-config', '--baz'], + '--unicorn' + ); + expect(argValues).toEqual([]); + }); + + it('ignores the flag when no value is provided', () => { + const argValues = getArgValues( + ['-c', 'my-config', '--foo', '-b', 'bar', '--config'], + ['--config', '-c'] + ); + expect(argValues).toEqual(['my-config']); + }); +}); + +describe('getArgValue', () => { + it('retrieve the first arg value from the provided argv arguments', () => { + const argValues = getArgValue( + ['--config', 'my-config', '--foo', '-b', 'bar', '--config', 'other-config', '--baz'], + '--config' + ); + expect(argValues).toEqual('my-config'); + }); + + it('accept aliases', () => { + const argValues = getArgValue( + ['-c', 'my-config', '--foo', '-b', 'bar', '--config', 'other-config', '--baz'], + ['--config', '-c'] + ); + expect(argValues).toEqual('my-config'); + }); + + it('returns undefined the arg is not found', () => { + const argValues = getArgValue( + ['--config', 'my-config', '--foo', '-b', 'bar', '-c', 'other-config', '--baz'], + '--unicorn' + ); + expect(argValues).toBeUndefined(); + }); +}); diff --git a/src/plugins/data/public/search/request_timeout_error.ts b/packages/kbn-apm-config-loader/src/utils/read_argv.ts similarity index 61% rename from src/plugins/data/public/search/request_timeout_error.ts rename to packages/kbn-apm-config-loader/src/utils/read_argv.ts index 92894deb4f0ff..9a74d5344a0fc 100644 --- a/src/plugins/data/public/search/request_timeout_error.ts +++ b/packages/kbn-apm-config-loader/src/utils/read_argv.ts @@ -17,14 +17,20 @@ * 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'; +export const getArgValues = (argv: string[], flag: string | string[]): string[] => { + const flags = typeof flag === 'string' ? [flag] : flag; + const values: string[] = []; + for (let i = 0; i < argv.length; i++) { + if (flags.includes(argv[i]) && argv[i + 1]) { + values.push(argv[++i]); + } + } + return values; +}; + +export const getArgValue = (argv: string[], flag: string | string[]): string | undefined => { + const values = getArgValues(argv, flag); + if (values.length) { + return values[0]; } -} +}; diff --git a/packages/kbn-apm-config-loader/src/utils/read_config.test.ts b/packages/kbn-apm-config-loader/src/utils/read_config.test.ts new file mode 100644 index 0000000000000..7320e5dcbd6ce --- /dev/null +++ b/packages/kbn-apm-config-loader/src/utils/read_config.test.ts @@ -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 { relative, resolve } from 'path'; +import { getConfigFromFiles } from './read_config'; + +const fixtureFile = (name: string) => resolve(__dirname, '..', '..', '__fixtures__', name); + +test('reads single yaml from file system and parses to json', () => { + const config = getConfigFromFiles([fixtureFile('config.yml')]); + + expect(config).toMatchSnapshot(); +}); + +test('returns a deep object', () => { + const config = getConfigFromFiles([fixtureFile('config_flat.yml')]); + + expect(config).toMatchSnapshot(); +}); + +test('reads and merges multiple yaml files from file system and parses to json', () => { + const config = getConfigFromFiles([fixtureFile('one.yml'), fixtureFile('two.yml')]); + + expect(config).toMatchSnapshot(); +}); + +test('should inject an environment variable value when setting a value with ${ENV_VAR}', () => { + process.env.KBN_ENV_VAR1 = 'val1'; + process.env.KBN_ENV_VAR2 = 'val2'; + + const config = getConfigFromFiles([fixtureFile('en_var_ref_config.yml')]); + + delete process.env.KBN_ENV_VAR1; + delete process.env.KBN_ENV_VAR2; + + expect(config).toMatchSnapshot(); +}); + +test('should throw an exception when referenced environment variable in a config value does not exist', () => { + expect(() => + getConfigFromFiles([fixtureFile('en_var_ref_config.yml')]) + ).toThrowErrorMatchingSnapshot(); +}); + +describe('different cwd()', () => { + const originalCwd = process.cwd(); + const tempCwd = resolve(__dirname); + + beforeAll(() => process.chdir(tempCwd)); + afterAll(() => process.chdir(originalCwd)); + + test('resolves relative files based on the cwd', () => { + const relativePath = relative(tempCwd, fixtureFile('one.yml')); + const config = getConfigFromFiles([relativePath]); + + expect(config).toMatchSnapshot(); + }); + + test('fails to load relative paths, not found because of the cwd', () => { + const relativePath = relative(resolve(__dirname, '..', '..'), fixtureFile('one.yml')); + expect(() => getConfigFromFiles([relativePath])).toThrowError(/ENOENT/); + }); +}); diff --git a/packages/kbn-apm-config-loader/src/utils/read_config.ts b/packages/kbn-apm-config-loader/src/utils/read_config.ts new file mode 100644 index 0000000000000..825bfd60181bf --- /dev/null +++ b/packages/kbn-apm-config-loader/src/utils/read_config.ts @@ -0,0 +1,64 @@ +/* + * 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 { readFileSync } from 'fs'; +import { safeLoad } from 'js-yaml'; + +import { set } from '@elastic/safer-lodash-set'; +import { isPlainObject } from 'lodash'; +import { ensureDeepObject } from './ensure_deep_object'; + +const readYaml = (path: string) => safeLoad(readFileSync(path, 'utf8')); + +function replaceEnvVarRefs(val: string) { + return val.replace(/\$\{(\w+)\}/g, (match, envVarName) => { + const envVarValue = process.env[envVarName]; + if (envVarValue !== undefined) { + return envVarValue; + } + + throw new Error(`Unknown environment variable referenced in config : ${envVarName}`); + }); +} + +function merge(target: Record, value: any, key?: string) { + if ((isPlainObject(value) || Array.isArray(value)) && Object.keys(value).length > 0) { + for (const [subKey, subVal] of Object.entries(value)) { + merge(target, subVal, key ? `${key}.${subKey}` : subKey); + } + } else if (key !== undefined) { + set(target, key, typeof value === 'string' ? replaceEnvVarRefs(value) : value); + } + + return target; +} + +/** @internal */ +export const getConfigFromFiles = (configFiles: readonly string[]): Record => { + let mergedYaml: Record = {}; + + for (const configFile of configFiles) { + const yaml = readYaml(configFile); + if (yaml !== null) { + mergedYaml = merge(mergedYaml, yaml); + } + } + + return ensureDeepObject(mergedYaml); +}; diff --git a/packages/kbn-apm-config-loader/tsconfig.json b/packages/kbn-apm-config-loader/tsconfig.json new file mode 100644 index 0000000000000..ba00ddfa6adb6 --- /dev/null +++ b/packages/kbn-apm-config-loader/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "outDir": "./target", + "stripInternal": false, + "declarationMap": true, + "types": ["jest", "node"] + }, + "include": ["./src/**/*.ts"], + "exclude": ["target"] +} diff --git a/packages/kbn-apm-config-loader/yarn.lock b/packages/kbn-apm-config-loader/yarn.lock new file mode 120000 index 0000000000000..3f82ebc9cdbae --- /dev/null +++ b/packages/kbn-apm-config-loader/yarn.lock @@ -0,0 +1 @@ +../../yarn.lock \ No newline at end of file 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/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/apm.js b/src/apm.js index effa6c77d7614..8a0c010d993f1 100644 --- a/src/apm.js +++ b/src/apm.js @@ -18,67 +18,11 @@ */ const { join } = require('path'); -const { readFileSync } = require('fs'); -const { execSync } = require('child_process'); -const { merge } = require('lodash'); -const { name, version, build } = require('../package.json'); +const { name, build } = require('../package.json'); +const { loadConfiguration } = require('@kbn/apm-config-loader'); const ROOT_DIR = join(__dirname, '..'); - -function gitRev() { - try { - return execSync('git rev-parse --short HEAD', { - encoding: 'utf-8', - stdio: ['ignore', 'pipe', 'ignore'], - }).trim(); - } catch (e) { - return null; - } -} - -function devConfig() { - try { - const apmDevConfigPath = join(ROOT_DIR, 'config', 'apm.dev.js'); - return require(apmDevConfigPath); // eslint-disable-line import/no-dynamic-require - } catch (e) { - return {}; - } -} - -const apmConfig = merge( - { - active: false, - serverUrl: 'https://f1542b814f674090afd914960583265f.apm.us-central1.gcp.cloud.es.io:443', - // The secretToken below is intended to be hardcoded in this file even though - // it makes it public. This is not a security/privacy issue. Normally we'd - // instead disable the need for a secretToken in the APM Server config where - // the data is transmitted to, but due to how it's being hosted, it's easier, - // for now, to simply leave it in. - secretToken: 'R0Gjg46pE9K9wGestd', - globalLabels: {}, - breakdownMetrics: true, - centralConfig: false, - logUncaughtExceptions: true, - }, - devConfig() -); - -try { - const filename = join(ROOT_DIR, 'data', 'uuid'); - apmConfig.globalLabels.kibana_uuid = readFileSync(filename, 'utf-8'); -} catch (e) {} // eslint-disable-line no-empty - -const rev = gitRev(); -if (rev !== null) apmConfig.globalLabels.git_rev = rev; - -function getConfig(serviceName) { - return { - ...apmConfig, - ...{ - serviceName: `${serviceName}-${version.replace(/\./g, '_')}`, - }, - }; -} +let apmConfig; /** * Flag to disable APM RUM support on all kibana builds by default @@ -86,12 +30,24 @@ function getConfig(serviceName) { const isKibanaDistributable = Boolean(build && build.distributable === true); module.exports = function (serviceName = name) { - if (process.env.kbnWorkerType === 'optmzr') return; - - const conf = getConfig(serviceName); + if (process.env.kbnWorkerType === 'optmzr') { + return; + } + apmConfig = loadConfiguration(process.argv, ROOT_DIR, isKibanaDistributable); + const conf = apmConfig.getConfig(serviceName); require('elastic-apm-node').start(conf); }; -module.exports.getConfig = getConfig; +module.exports.getConfig = (serviceName) => { + // integration test runner starts a kibana server that import the module without initializing APM. + // so we need to check initialization of the config. + // note that we can't just load the configuration during this module's import + // because jest IT are ran with `--config path-to-jest-config.js` which conflicts with the CLI's `config` arg + // causing the config loader to try to load the jest js config as yaml and throws. + if (apmConfig) { + return apmConfig.getConfig(serviceName); + } + return {}; +}; module.exports.isKibanaDistributable = isKibanaDistributable; 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/flush.js b/src/dev/code_coverage/ingest_coverage/team_assignment/flush.js new file mode 100644 index 0000000000000..5150ac2e655f2 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/flush.js @@ -0,0 +1,44 @@ +/* + * 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 { writeFileSync } from 'fs'; +import shell from 'shelljs'; +import { tryCatch } from '../either'; +import { id } from '../utils'; + +const encoding = 'utf8'; +const appendUtf8 = { flag: 'a', encoding }; + +export const flush = (dest) => (log) => (assignments) => { + log.verbose(`\n### Flushing assignments to: \n\t${dest}`); + + 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/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index a94766ef06926..feeb8e0bf6e4c 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -38,12 +38,8 @@ export default () => prod: Joi.boolean().default(Joi.ref('$prod')), }).default(), - dev: Joi.object({ - basePathProxyTarget: Joi.number().default(5603), - }).default(), - + dev: HANDLED_IN_NEW_PLATFORM, pid: HANDLED_IN_NEW_PLATFORM, - csp: HANDLED_IN_NEW_PLATFORM, server: Joi.object({ @@ -125,10 +121,7 @@ export default () => ops: Joi.object({ interval: Joi.number().default(5000), - cGroupOverrides: Joi.object().keys({ - cpuPath: Joi.string().default(), - cpuAcctPath: Joi.string().default(), - }), + cGroupOverrides: HANDLED_IN_NEW_PLATFORM, }).default(), // still used by the legacy i18n mixin @@ -139,15 +132,8 @@ export default () => }).default(), path: HANDLED_IN_NEW_PLATFORM, - - stats: Joi.object({ - maximumWaitTimeForAllCollectorsInS: Joi.number().default(60), - }).default(), - - status: Joi.object({ - allowAnonymous: Joi.boolean().default(false), - }).default(), - + stats: HANDLED_IN_NEW_PLATFORM, + status: HANDLED_IN_NEW_PLATFORM, map: HANDLED_IN_NEW_PLATFORM, i18n: Joi.object({ @@ -163,8 +149,5 @@ export default () => autocompleteTimeout: Joi.number().integer().min(1).default(1000), }).default(), - savedObjects: Joi.object({ - maxImportPayloadBytes: Joi.number().default(10485760), - maxImportExportSize: Joi.number().default(10000), - }).default(), + savedObjects: HANDLED_IN_NEW_PLATFORM, }).default(); 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/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/tile_map/kibana.json b/src/plugins/tile_map/kibana.json index acf1424229c8e..d65cb65fd3042 100644 --- a/src/plugins/tile_map/kibana.json +++ b/src/plugins/tile_map/kibana.json @@ -10,7 +10,8 @@ "expressions", "mapsLegacy", "kibanaLegacy", - "data" + "data", + "share" ], "requiredBundles": [ "kibanaUtils", diff --git a/src/plugins/tile_map/public/get_deprecation_message.tsx b/src/plugins/tile_map/public/get_deprecation_message.tsx new file mode 100644 index 0000000000000..d5c12075258c2 --- /dev/null +++ b/src/plugins/tile_map/public/get_deprecation_message.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 { i18n } from '@kbn/i18n'; +import React from 'react'; +import { UrlGeneratorContract } from 'src/plugins/share/public'; +import { getCoreService, getQueryService, getShareService } from './services'; +import { indexPatterns } from '../../data/public'; +import { Vis } from '../../visualizations/public'; +import { LegacyMapDeprecationMessage } from '../../maps_legacy/public'; + +export function getDeprecationMessage(vis: Vis) { + let mapsTileMapUrlGenerator: UrlGeneratorContract<'MAPS_APP_TILE_MAP_URL_GENERATOR'> | 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/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/cypress/support/step_definitions/csm/csm_filters.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_filters.ts index c17845fd61468..75974ef9c202c 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_filters.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_filters.ts @@ -14,16 +14,40 @@ When(/^the user filters by "([^"]*)"$/, (filterName) => { cy.get('.euiStat__title-isLoading').should('not.be.visible'); cy.get(`#local-filter-${filterName}`).click(); - if (filterName === 'os') { - cy.get('span.euiSelectableListItem__text', DEFAULT_TIMEOUT) - .contains('Mac OS X') - .click(); - } else { - cy.get('span.euiSelectableListItem__text', DEFAULT_TIMEOUT) - .contains('DE') - .click(); - } - cy.get('[data-cy=applyFilter]').click(); + cy.get(`#local-filter-popover-${filterName}`, DEFAULT_TIMEOUT).within(() => { + if (filterName === 'os') { + const osItem = cy.get('li.euiSelectableListItem', DEFAULT_TIMEOUT).eq(2); + osItem.should('have.text', 'Mac OS X8 '); + osItem.click(); + + // sometimes click doesn't work as expected so we need to retry here + osItem.invoke('attr', 'aria-selected').then((val) => { + if (val === 'false') { + cy.get('li.euiSelectableListItem', DEFAULT_TIMEOUT).eq(2).click(); + } + }); + } else { + const deItem = cy.get('li.euiSelectableListItem', DEFAULT_TIMEOUT).eq(0); + deItem.should('have.text', 'DE28 '); + deItem.click(); + + // sometimes click doesn't work as expected so we need to retry here + deItem.invoke('attr', 'aria-selected').then((val) => { + if (val === 'false') { + cy.get('li.euiSelectableListItem', DEFAULT_TIMEOUT).eq(0).click(); + } + }); + } + cy.get('[data-cy=applyFilter]').click(); + }); + + cy.get(`div#local-filter-values-${filterName}`, DEFAULT_TIMEOUT).within( + () => { + cy.get('span.euiBadge__content') + .eq(0) + .should('have.text', filterName === 'os' ? 'Mac OS X' : 'DE'); + } + ); }); Then(/^it filters the client metrics "([^"]*)"$/, (filterName) => { diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts index 4169149bc7339..b3899a5649b72 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts @@ -5,15 +5,13 @@ */ import { When, Then } from 'cypress-cucumber-preprocessor/steps'; -import { DEFAULT_TIMEOUT } from '../apm'; import { verifyClientMetrics } from './client_metrics_helper'; +import { DEFAULT_TIMEOUT } from './csm_dashboard'; -When('the user changes the selected service name', (filterName) => { +When('the user changes the selected service name', () => { // wait for all loading to finish cy.get('kbnLoadingIndicator').should('not.be.visible'); - cy.get(`[data-cy=serviceNameFilter]`, { timeout: DEFAULT_TIMEOUT }).select( - 'client' - ); + cy.get(`[data-cy=serviceNameFilter]`, DEFAULT_TIMEOUT).select('client'); }); Then(`it displays relevant client metrics`, () => { diff --git a/x-pack/plugins/apm/e2e/ingest-data/replay.js b/x-pack/plugins/apm/e2e/ingest-data/replay.js index 74c86b1b09ab4..326cb739e23c6 100644 --- a/x-pack/plugins/apm/e2e/ingest-data/replay.js +++ b/x-pack/plugins/apm/e2e/ingest-data/replay.js @@ -96,7 +96,7 @@ function setRumAgent(item) { if (item.body) { item.body = item.body.replace( '"name":"client"', - '"name":"opbean-client-rum"' + '"name":"elastic-frontend"' ); } } 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/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx index a77d27c4bc883..bc1e0a86f17db 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx @@ -6,10 +6,12 @@ import * as React from 'react'; import numeral from '@elastic/numeral'; import styled from 'styled-components'; +import { useContext, useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiToolTip } from '@elastic/eui'; import { useFetcher } from '../../../../hooks/useFetcher'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { I18LABELS } from '../translations'; +import { CsmSharedContext } from '../CsmSharedContext'; const ClFlexGroup = styled(EuiFlexGroup)` flex-direction: row; @@ -45,6 +47,12 @@ export function ClientMetrics() { [start, end, uiFilters, searchTerm] ); + const { setSharedData } = useContext(CsmSharedContext); + + useEffect(() => { + setSharedData({ totalPageViews: data?.pageViews?.value ?? 0 }); + }, [data, setSharedData]); + const STAT_STYLE = { width: '240px' }; return ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CsmSharedContext/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CsmSharedContext/index.tsx new file mode 100644 index 0000000000000..3d445104d6d10 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CsmSharedContext/index.tsx @@ -0,0 +1,45 @@ +/* + * 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, { createContext, useMemo, useState } from 'react'; + +interface SharedData { + totalPageViews: number; +} + +interface Index { + sharedData: SharedData; + setSharedData: (data: SharedData) => void; +} + +const defaultContext: Index = { + sharedData: { totalPageViews: 0 }, + setSharedData: (d) => { + throw new Error( + 'setSharedData was not initialized, set it when you invoke the context' + ); + }, +}; + +export const CsmSharedContext = createContext(defaultContext); + +export function CsmSharedContextProvider({ + children, +}: { + children: JSX.Element[]; +}) { + const [newData, setNewData] = useState({ totalPageViews: 0 }); + + const setSharedData = React.useCallback((data: SharedData) => { + setNewData(data); + }, []); + + const value = useMemo(() => { + return { sharedData: newData, setSharedData }; + }, [newData, setSharedData]); + + return ; +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx new file mode 100644 index 0000000000000..805b328cb1fb0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext, useState } from 'react'; +import { + EuiBasicTable, + EuiFlexItem, + EuiFlexGroup, + EuiSpacer, + EuiTitle, + EuiStat, + EuiToolTip, +} from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useFetcher } from '../../../../hooks/useFetcher'; +import { I18LABELS } from '../translations'; +import { CsmSharedContext } from '../CsmSharedContext'; + +export function JSErrors() { + const { urlParams, uiFilters } = useUrlParams(); + + const { start, end, serviceName } = urlParams; + + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 5 }); + + const { data, status } = useFetcher( + (callApmApi) => { + if (start && end && serviceName) { + return callApmApi({ + pathname: '/api/apm/rum-client/js-errors', + params: { + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + pageSize: String(pagination.pageSize), + pageIndex: String(pagination.pageIndex), + }, + }, + }); + } + return Promise.resolve(null); + }, + [start, end, serviceName, uiFilters, pagination] + ); + + const { + sharedData: { totalPageViews }, + } = useContext(CsmSharedContext); + + const items = (data?.items ?? []).map(({ errorMessage, count }) => ({ + errorMessage, + percent: i18n.translate('xpack.apm.rum.jsErrors.percent', { + defaultMessage: '{pageLoadPercent} %', + values: { pageLoadPercent: ((count / totalPageViews) * 100).toFixed(1) }, + }), + })); + + const cols = [ + { + field: 'errorMessage', + name: I18LABELS.errorMessage, + }, + { + name: I18LABELS.impactedPageLoads, + field: 'percent', + align: 'right' as const, + }, + ]; + + const onTableChange = ({ + page, + }: { + page: { size: number; index: number }; + }) => { + setPagination({ + pageIndex: page.index, + pageSize: page.size, + }); + }; + + return ( + <> + +

{I18LABELS.jsErrors}

+
+ + + + + <>{numeral(data?.totalErrors ?? 0).format('0 a')} + + } + description={I18LABELS.totalErrors} + isLoading={status !== 'success'} + /> + + + + {' '} + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/index.tsx new file mode 100644 index 0000000000000..34cb6338eb948 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/index.tsx @@ -0,0 +1,22 @@ +/* + * 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 { EuiFlexItem, EuiPanel, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; +import { JSErrors } from './JSErrors'; + +export function ImpactfulMetrics() { + return ( + + + + + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx index ddef5cd08e521..37522b06970c1 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx @@ -19,6 +19,7 @@ import { I18LABELS } from './translations'; import { VisitorBreakdown } from './VisitorBreakdown'; import { UXMetrics } from './UXMetrics'; import { VisitorBreakdownMap } from './VisitorBreakdownMap'; +import { ImpactfulMetrics } from './ImpactfulMetrics'; export function RumDashboard() { return ( @@ -66,6 +67,9 @@ export function RumDashboard() { + + + ); } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx index 9abf792d7a0cf..28bb5307b6e8c 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { RumOverview } from '../RumDashboard'; import { RumHeader } from './RumHeader'; +import { CsmSharedContextProvider } from './CsmSharedContext'; export const UX_LABEL = i18n.translate('xpack.apm.ux.title', { defaultMessage: 'User Experience', @@ -17,16 +18,18 @@ export const UX_LABEL = i18n.translate('xpack.apm.ux.title', { export function RumHome() { return (
- - - - -

{UX_LABEL}

-
-
-
-
- + + + + + +

{UX_LABEL}

+
+
+
+
+ +
); } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts index 714788ef468c6..f92a1d5a5945b 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts @@ -37,6 +37,18 @@ export const I18LABELS = { defaultMessage: 'Page load distribution', } ), + jsErrors: i18n.translate( + 'xpack.apm.rum.dashboard.impactfulMetrics.jsErrors', + { + defaultMessage: 'JavaScript errors', + } + ), + highTrafficPages: i18n.translate( + 'xpack.apm.rum.dashboard.impactfulMetrics.highTrafficPages', + { + defaultMessage: 'High traffic pages', + } + ), resetZoom: i18n.translate('xpack.apm.rum.dashboard.resetZoom.label', { defaultMessage: 'Reset zoom', }), @@ -105,6 +117,21 @@ export const I18LABELS = { noResults: i18n.translate('xpack.apm.rum.filters.url.noResults', { defaultMessage: 'No results available', }), + totalErrors: i18n.translate('xpack.apm.rum.jsErrors.totalErrors', { + defaultMessage: 'Total errors', + }), + errorRate: i18n.translate('xpack.apm.rum.jsErrors.errorRate', { + defaultMessage: 'Error rate', + }), + errorMessage: i18n.translate('xpack.apm.rum.jsErrors.errorMessage', { + defaultMessage: 'Error message', + }), + impactedPageLoads: i18n.translate( + 'xpack.apm.rum.jsErrors.impactedPageLoads', + { + defaultMessage: 'Impacted page loads', + } + ), }; export const VisitorBreakdownLabel = i18n.translate( 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/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx index ed8d865d2d288..ee240cfef3b21 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx @@ -19,6 +19,7 @@ const BadgeText = styled.div` interface Props { value: string[]; onRemove: (val: string) => void; + name: string; } const removeFilterLabel = i18n.translate( @@ -26,9 +27,9 @@ const removeFilterLabel = i18n.translate( { defaultMessage: 'Remove filter' } ); -function FilterBadgeList({ onRemove, value }: Props) { +function FilterBadgeList({ onRemove, value, name }: Props) { return ( - + {value.map((val) => ( {(list, search) => ( - + @@ -159,6 +159,7 @@ function Filter({ name, title, options, onChange, value, showCount }: Props) { {value.length ? ( <> { onChange(value.filter((v) => val !== v)); }} diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap index ceffb4f4d6654..66cfa954965d2 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap @@ -72,6 +72,97 @@ Object { } `; +exports[`rum client dashboard queries fetches js errors 1`] = ` +Object { + "apm": Object { + "events": Array [ + "error", + ], + }, + "body": Object { + "aggs": Object { + "errors": Object { + "aggs": Object { + "bucket_truncate": Object { + "bucket_sort": Object { + "from": 0, + "size": 5, + }, + }, + "sample": Object { + "top_hits": Object { + "_source": Array [ + "error.exception.message", + "error.exception.type", + "error.grouping_key", + "@timestamp", + ], + "size": 1, + "sort": Array [ + Object { + "@timestamp": "desc", + }, + ], + }, + }, + }, + "terms": Object { + "field": "error.grouping_key", + "size": 500, + }, + }, + "totalErrorGroups": Object { + "cardinality": Object { + "field": "error.grouping_key", + }, + }, + "totalErrorPages": Object { + "cardinality": Object { + "field": "transaction.id", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "agent.name": "rum-js", + }, + }, + Object { + "term": Object { + "transaction.type": "page-load", + }, + }, + Object { + "term": Object { + "service.language.name": "javascript", + }, + }, + Object { + "term": Object { + "my.custom.ui.filter": "foo-bar", + }, + }, + ], + }, + }, + "size": 0, + "track_total_hits": true, + }, +} +`; + exports[`rum client dashboard queries fetches long task metrics 1`] = ` Object { "apm": Object { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts b/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts new file mode 100644 index 0000000000000..0540ea4bf09dd --- /dev/null +++ b/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts @@ -0,0 +1,99 @@ +/* + * 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 { mergeProjection } from '../../projections/util/merge_projection'; +import { + Setup, + SetupTimeRange, + SetupUIFilters, +} from '../helpers/setup_request'; +import { getRumErrorsProjection } from '../../projections/rum_page_load_transactions'; +import { + ERROR_EXC_MESSAGE, + ERROR_EXC_TYPE, + ERROR_GROUP_ID, + TRANSACTION_ID, +} from '../../../common/elasticsearch_fieldnames'; + +export async function getJSErrors({ + setup, + pageSize, + pageIndex, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; + pageSize: number; + pageIndex: number; +}) { + const projection = getRumErrorsProjection({ + setup, + }); + + const params = mergeProjection(projection, { + body: { + size: 0, + track_total_hits: true, + aggs: { + totalErrorGroups: { + cardinality: { + field: ERROR_GROUP_ID, + }, + }, + totalErrorPages: { + cardinality: { + field: TRANSACTION_ID, + }, + }, + errors: { + terms: { + field: ERROR_GROUP_ID, + size: 500, + }, + aggs: { + bucket_truncate: { + bucket_sort: { + size: pageSize, + from: pageIndex * pageSize, + }, + }, + sample: { + top_hits: { + _source: [ + ERROR_EXC_MESSAGE, + ERROR_EXC_TYPE, + ERROR_GROUP_ID, + '@timestamp', + ], + sort: [{ '@timestamp': 'desc' as const }], + size: 1, + }, + }, + }, + }, + }, + }, + }); + + const { apmEventClient } = setup; + + const response = await apmEventClient.search(params); + + const { totalErrorGroups, totalErrorPages, errors } = + response.aggregations ?? {}; + + return { + totalErrorPages: totalErrorPages?.value ?? 0, + totalErrors: response.hits.total.value ?? 0, + totalErrorGroups: totalErrorGroups?.value ?? 0, + items: errors?.buckets.map(({ sample, doc_count: count }) => { + return { + count, + errorMessage: (sample.hits.hits[0]._source as { + error: { exception: Array<{ message: string }> }; + }).error.exception?.[0].message, + }; + }), + }; +} diff --git a/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts b/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts index 14cec21cceb79..23d2cb829b8d5 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts @@ -14,6 +14,7 @@ import { getPageLoadDistribution } from './get_page_load_distribution'; import { getRumServices } from './get_rum_services'; import { getLongTaskMetrics } from './get_long_task_metrics'; import { getWebCoreVitals } from './get_web_core_vitals'; +import { getJSErrors } from './get_js_errors'; describe('rum client dashboard queries', () => { let mock: SearchParamsMock; @@ -79,4 +80,15 @@ describe('rum client dashboard queries', () => { ); expect(mock.params).toMatchSnapshot(); }); + + it('fetches js errors', async () => { + mock = await inspectSearchParams((setup) => + getJSErrors({ + setup, + pageSize: 5, + pageIndex: 0, + }) + ); + expect(mock.params).toMatchSnapshot(); + }); }); diff --git a/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts b/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts index 3c3eaaca7efdb..a8505337e8aec 100644 --- a/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts +++ b/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts @@ -11,7 +11,9 @@ import { } from '../../server/lib/helpers/setup_request'; import { SPAN_TYPE, + AGENT_NAME, TRANSACTION_TYPE, + SERVICE_LANGUAGE_NAME, } from '../../common/elasticsearch_fieldnames'; import { rangeFilter } from '../../common/utils/range_filter'; import { ProcessorEvent } from '../../common/processor_event'; @@ -90,3 +92,36 @@ export function getRumLongTasksProjection({ }, }; } + +export function getRumErrorsProjection({ + setup, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; +}) { + const { start, end, uiFiltersES } = setup; + + const bool = { + filter: [ + { range: rangeFilter(start, end) }, + { term: { [AGENT_NAME]: 'rum-js' } }, + { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, + { + term: { + [SERVICE_LANGUAGE_NAME]: 'javascript', + }, + }, + ...uiFiltersES, + ], + }; + + return { + apm: { + events: [ProcessorEvent.error], + }, + body: { + query: { + bool, + }, + }, + }; +} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index f975ab177f147..0560b977e708e 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -69,17 +69,6 @@ import { listCustomLinksRoute, customLinkTransactionRoute, } from './settings/custom_link'; -import { - rumClientMetricsRoute, - rumPageViewsTrendRoute, - rumPageLoadDistributionRoute, - rumPageLoadDistBreakdownRoute, - rumServicesRoute, - rumVisitorsBreakdownRoute, - rumWebCoreVitals, - rumUrlSearch, - rumLongTaskMetrics, -} from './rum_client'; import { observabilityOverviewHasDataRoute, observabilityOverviewRoute, @@ -89,6 +78,18 @@ import { createAnomalyDetectionJobsRoute, anomalyDetectionEnvironmentsRoute, } from './settings/anomaly_detection'; +import { + rumClientMetricsRoute, + rumJSErrors, + rumLongTaskMetrics, + rumPageLoadDistBreakdownRoute, + rumPageLoadDistributionRoute, + rumPageViewsTrendRoute, + rumServicesRoute, + rumUrlSearch, + rumVisitorsBreakdownRoute, + rumWebCoreVitals, +} from './rum_client'; const createApmApi = () => { const api = createApi() @@ -165,7 +166,16 @@ const createApmApi = () => { .add(listCustomLinksRoute) .add(customLinkTransactionRoute) - // Rum Overview + // Observability dashboard + .add(observabilityOverviewHasDataRoute) + .add(observabilityOverviewRoute) + + // Anomaly detection + .add(anomalyDetectionJobsRoute) + .add(createAnomalyDetectionJobsRoute) + .add(anomalyDetectionEnvironmentsRoute) + + // User Experience app api routes .add(rumOverviewLocalFiltersRoute) .add(rumPageViewsTrendRoute) .add(rumPageLoadDistributionRoute) @@ -174,17 +184,9 @@ const createApmApi = () => { .add(rumServicesRoute) .add(rumVisitorsBreakdownRoute) .add(rumWebCoreVitals) + .add(rumJSErrors) .add(rumUrlSearch) - .add(rumLongTaskMetrics) - - // Observability dashboard - .add(observabilityOverviewHasDataRoute) - .add(observabilityOverviewRoute) - - // Anomaly detection - .add(anomalyDetectionJobsRoute) - .add(createAnomalyDetectionJobsRoute) - .add(anomalyDetectionEnvironmentsRoute); + .add(rumLongTaskMetrics); return api; }; diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index e3a846f9fb5c7..c0351991e4c0d 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -15,6 +15,7 @@ import { getPageLoadDistBreakdown } from '../lib/rum_client/get_pl_dist_breakdow import { getRumServices } from '../lib/rum_client/get_rum_services'; import { getVisitorBreakdown } from '../lib/rum_client/get_visitor_breakdown'; import { getWebCoreVitals } from '../lib/rum_client/get_web_core_vitals'; +import { getJSErrors } from '../lib/rum_client/get_js_errors'; import { getLongTaskMetrics } from '../lib/rum_client/get_long_task_metrics'; import { getUrlSearch } from '../lib/rum_client/get_url_search'; @@ -191,3 +192,27 @@ export const rumUrlSearch = createRoute(() => ({ return getUrlSearch({ setup, urlQuery }); }, })); + +export const rumJSErrors = createRoute(() => ({ + path: '/api/apm/rum-client/js-errors', + params: { + query: t.intersection([ + uiFiltersRt, + rangeRt, + t.type({ pageSize: t.string, pageIndex: t.string }), + ]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const { + query: { pageSize, pageIndex }, + } = context.params; + + return getJSErrors({ + setup, + pageSize: Number(pageSize), + pageIndex: Number(pageIndex), + }); + }, +})); diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts index 93f8b115256b4..534321201938d 100644 --- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts +++ b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts @@ -153,6 +153,11 @@ export interface AggregationOptionsByType { keyed?: boolean; hdr?: { number_of_significant_value_digits: number }; } & AggregationSourceOptions; + bucket_sort: { + sort?: SortOptions; + from?: number; + size?: number; + }; } type AggregationType = keyof AggregationOptionsByType; @@ -329,6 +334,7 @@ interface AggregationResponsePart< ? Array<{ key: number; value: number }> : Record; }; + bucket_sort: undefined; } // Type for debugging purposes. If you see an error in AggregationResponseMap diff --git a/x-pack/plugins/apm/typings/elasticsearch/index.ts b/x-pack/plugins/apm/typings/elasticsearch/index.ts index 064b684cf9aa6..9a05fe631e888 100644 --- a/x-pack/plugins/apm/typings/elasticsearch/index.ts +++ b/x-pack/plugins/apm/typings/elasticsearch/index.ts @@ -7,11 +7,22 @@ import { SearchParams, SearchResponse } from 'elasticsearch'; import { AggregationResponseMap, AggregationInputMap } from './aggregations'; +interface CollapseQuery { + field: string; + inner_hits: { + name: string; + size?: number; + sort?: [{ date: 'asc' | 'desc' }]; + }; + max_concurrent_group_searches?: number; +} + export interface ESSearchBody { query?: any; size?: number; aggs?: AggregationInputMap; track_total_hits?: boolean | number; + collapse?: CollapseQuery; } export type ESSearchRequest = Omit & { 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/common/types/index.ts b/x-pack/plugins/enterprise_search/common/types/index.ts index d5774adc0d516..1006d39138759 100644 --- a/x-pack/plugins/enterprise_search/common/types/index.ts +++ b/x-pack/plugins/enterprise_search/common/types/index.ts @@ -30,3 +30,14 @@ export interface IConfiguredLimits { appSearch: IAppSearchConfiguredLimits; workplaceSearch: IWorkplaceSearchConfiguredLimits; } + +export interface IMetaPage { + current: number; + size: number; + total_pages: number; + total_results: number; +} + +export interface IMeta { + page: IMetaPage; +} 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/credentials/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts new file mode 100644 index 0000000000000..92d14f7275185 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; + +export const ADMIN = 'admin'; +export const PRIVATE = 'private'; +export const SEARCH = 'search'; + +export const TOKEN_TYPE_DESCRIPTION = { + [SEARCH]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.search.description', { + defaultMessage: 'Public Search Keys are used for search endpoints only.', + }), + [PRIVATE]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.private.description', { + defaultMessage: + 'Private API Keys are used for read and/or write access on one or more Engines.', + }), + [ADMIN]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.admin.description', { + defaultMessage: 'Private Admin Keys are used to interact with the Credentials API.', + }), +}; + +export const TOKEN_TYPE_DISPLAY_NAMES = { + [SEARCH]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.search.name', { + defaultMessage: 'Public Search Key', + }), + [PRIVATE]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.private.name', { + defaultMessage: 'Private API Key', + }), + [ADMIN]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.admin.name', { + defaultMessage: 'Private Admin Key', + }), +}; + +export const TOKEN_TYPE_INFO = [ + { value: SEARCH, text: TOKEN_TYPE_DISPLAY_NAMES[SEARCH] }, + { value: PRIVATE, text: TOKEN_TYPE_DISPLAY_NAMES[PRIVATE] }, + { value: ADMIN, text: TOKEN_TYPE_DISPLAY_NAMES[ADMIN] }, +]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts new file mode 100644 index 0000000000000..c5cb8a2c61759 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts @@ -0,0 +1,1196 @@ +/* + * 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 { resetContext } from 'kea'; + +import { CredentialsLogic } from './credentials_logic'; +import { ADMIN, PRIVATE } from './constants'; + +jest.mock('../../../shared/http', () => ({ + HttpLogic: { values: { http: { get: jest.fn(), delete: jest.fn() } } }, +})); +import { HttpLogic } from '../../../shared/http'; +jest.mock('../../../shared/flash_messages', () => ({ + flashAPIErrors: jest.fn(), +})); +import { flashAPIErrors } from '../../../shared/flash_messages'; + +describe('CredentialsLogic', () => { + const DEFAULT_VALUES = { + activeApiToken: { + name: '', + type: PRIVATE, + read: true, + write: true, + access_all_engines: true, + }, + activeApiTokenIsExisting: false, + activeApiTokenRawName: '', + apiTokens: [], + dataLoading: true, + engines: [], + formErrors: [], + isCredentialsDataComplete: false, + isCredentialsDetailsComplete: false, + meta: {}, + nameInputBlurred: false, + showCredentialsForm: false, + }; + + const mount = (defaults?: object) => { + if (!defaults) { + resetContext({}); + } else { + resetContext({ + defaults: { + enterprise_search: { + app_search: { + credentials_logic: { + ...defaults, + }, + }, + }, + }, + }); + } + CredentialsLogic.mount(); + }; + + const newToken = { + id: 1, + name: 'myToken', + type: PRIVATE, + read: true, + write: true, + access_all_engines: true, + engines: [], + }; + + const credentialsDetails = { + engines: [ + { name: 'engine1', type: 'indexed', language: 'english', result_fields: [] }, + { name: 'engine1', type: 'indexed', language: 'english', result_fields: [] }, + ], + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(CredentialsLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('addEngineName', () => { + const values = { + ...DEFAULT_VALUES, + activeApiToken: expect.any(Object), + }; + + describe('activeApiToken', () => { + it("should add an engine to the active api token's engine list", () => { + mount({ + activeApiToken: { + ...newToken, + engines: ['someEngine'], + }, + }); + + CredentialsLogic.actions.addEngineName('newEngine'); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { ...newToken, engines: ['someEngine', 'newEngine'] }, + }); + }); + + it("should create a new engines list if one doesn't exist", () => { + mount({ + activeApiToken: { + ...newToken, + engines: undefined, + }, + }); + + CredentialsLogic.actions.addEngineName('newEngine'); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { ...newToken, engines: ['newEngine'] }, + }); + }); + }); + }); + + describe('removeEngineName', () => { + describe('activeApiToken', () => { + const values = { + ...DEFAULT_VALUES, + activeApiToken: expect.any(Object), + }; + + it("should remove an engine from the active api token's engine list", () => { + mount({ + activeApiToken: { + ...newToken, + engines: ['someEngine', 'anotherEngine'], + }, + }); + + CredentialsLogic.actions.removeEngineName('someEngine'); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { ...newToken, engines: ['anotherEngine'] }, + }); + }); + + it('will not remove the engine if it is not found', () => { + mount({ + activeApiToken: { + ...newToken, + engines: ['someEngine', 'anotherEngine'], + }, + }); + + CredentialsLogic.actions.removeEngineName('notfound'); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { ...newToken, engines: ['someEngine', 'anotherEngine'] }, + }); + }); + + it('does not throw a type error if no engines are stored in state', () => { + mount({ + activeApiToken: {}, + }); + CredentialsLogic.actions.removeEngineName(''); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { engines: [] }, + }); + }); + }); + }); + + describe('setAccessAllEngines', () => { + const values = { + ...DEFAULT_VALUES, + activeApiToken: expect.any(Object), + }; + + describe('activeApiToken', () => { + it('should set the value of access_all_engines and clear out engines list if true', () => { + mount({ + activeApiToken: { + ...newToken, + access_all_engines: false, + engines: ['someEngine', 'anotherEngine'], + }, + }); + + CredentialsLogic.actions.setAccessAllEngines(true); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { ...newToken, engines: [], access_all_engines: true }, + }); + }); + + it('should set the value of access_all_engines and but maintain engines list if false', () => { + mount({ + activeApiToken: { + ...newToken, + access_all_engines: true, + engines: ['someEngine', 'anotherEngine'], + }, + }); + + CredentialsLogic.actions.setAccessAllEngines(false); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...newToken, + access_all_engines: false, + engines: ['someEngine', 'anotherEngine'], + }, + }); + }); + }); + }); + + describe('onApiKeyDelete', () => { + const values = { + ...DEFAULT_VALUES, + apiTokens: expect.any(Array), + }; + + describe('apiTokens', () => { + it('should remove specified token from apiTokens if name matches', () => { + mount({ + apiTokens: [newToken], + }); + + CredentialsLogic.actions.onApiKeyDelete(newToken.name); + expect(CredentialsLogic.values).toEqual({ + ...values, + apiTokens: [], + }); + }); + + it('should not remove specified token from apiTokens if name does not match', () => { + mount({ + apiTokens: [newToken], + }); + + CredentialsLogic.actions.onApiKeyDelete('foo'); + expect(CredentialsLogic.values).toEqual({ + ...values, + apiTokens: [newToken], + }); + }); + }); + }); + + describe('onApiTokenCreateSuccess', () => { + const values = { + ...DEFAULT_VALUES, + apiTokens: expect.any(Array), + activeApiToken: expect.any(Object), + activeApiTokenRawName: expect.any(String), + showCredentialsForm: expect.any(Boolean), + formErrors: expect.any(Array), + }; + + describe('apiTokens', () => { + const existingToken = { + name: 'some_token', + type: PRIVATE, + }; + + it('should add the provided token to the apiTokens list', () => { + mount({ + apiTokens: [existingToken], + }); + + CredentialsLogic.actions.onApiTokenCreateSuccess(newToken); + expect(CredentialsLogic.values).toEqual({ + ...values, + apiTokens: [existingToken, newToken], + }); + }); + }); + + describe('activeApiToken', () => { + // TODO It is weird that methods like this update activeApiToken but not activeApiTokenIsExisting... + it('should reset to the default value, which effectively clears out the current form', () => { + mount({ + activeApiToken: newToken, + }); + + CredentialsLogic.actions.onApiTokenCreateSuccess(newToken); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: DEFAULT_VALUES.activeApiToken, + }); + }); + }); + + describe('activeApiTokenRawName', () => { + it('should reset to the default value, which effectively clears out the current form', () => { + mount({ + activeApiTokenRawName: 'foo', + }); + + CredentialsLogic.actions.onApiTokenCreateSuccess(newToken); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiTokenRawName: DEFAULT_VALUES.activeApiTokenRawName, + }); + }); + }); + + describe('showCredentialsForm', () => { + it('should reset to the default value, which closes the credentials form', () => { + mount({ + showCredentialsForm: true, + }); + + CredentialsLogic.actions.onApiTokenCreateSuccess(newToken); + expect(CredentialsLogic.values).toEqual({ + ...values, + showCredentialsForm: false, + }); + }); + }); + + describe('formErrors', () => { + it('should reset `formErrors`', () => { + mount({ + formErrors: ['I am an error'], + }); + + CredentialsLogic.actions.onApiTokenCreateSuccess(newToken); + expect(CredentialsLogic.values).toEqual({ + ...values, + formErrors: [], + }); + }); + }); + }); + + describe('onApiTokenError', () => { + const values = { + ...DEFAULT_VALUES, + formErrors: expect.any(Array), + }; + + describe('formErrors', () => { + it('should set `formErrors`', () => { + mount({ + formErrors: ['I am an error'], + }); + + CredentialsLogic.actions.onApiTokenError(['I am the NEW error']); + expect(CredentialsLogic.values).toEqual({ + ...values, + formErrors: ['I am the NEW error'], + }); + }); + }); + }); + + describe('onApiTokenUpdateSuccess', () => { + const values = { + ...DEFAULT_VALUES, + apiTokens: expect.any(Array), + activeApiToken: expect.any(Object), + activeApiTokenRawName: expect.any(String), + showCredentialsForm: expect.any(Boolean), + }; + + describe('apiTokens', () => { + const existingToken = { + name: 'some_token', + type: PRIVATE, + }; + + it('should replace the existing token with the new token by name', () => { + mount({ + apiTokens: [newToken, existingToken], + }); + const updatedExistingToken = { + ...existingToken, + type: ADMIN, + }; + + CredentialsLogic.actions.onApiTokenUpdateSuccess(updatedExistingToken); + expect(CredentialsLogic.values).toEqual({ + ...values, + apiTokens: [newToken, updatedExistingToken], + }); + }); + + // TODO Not sure if this is a good behavior or not + it('if for some reason the existing token is not found, it adds a new token...', () => { + mount({ + apiTokens: [newToken, existingToken], + }); + const brandNewToken = { + name: 'brand new token', + type: ADMIN, + }; + + CredentialsLogic.actions.onApiTokenUpdateSuccess(brandNewToken); + expect(CredentialsLogic.values).toEqual({ + ...values, + apiTokens: [newToken, existingToken, brandNewToken], + }); + }); + }); + + describe('activeApiToken', () => { + it('should reset to the default value, which effectively clears out the current form', () => { + mount({ + activeApiToken: newToken, + }); + + CredentialsLogic.actions.onApiTokenUpdateSuccess({ ...newToken, type: ADMIN }); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: DEFAULT_VALUES.activeApiToken, + }); + }); + }); + + describe('activeApiTokenRawName', () => { + it('should reset to the default value, which effectively clears out the current form', () => { + mount({ + activeApiTokenRawName: 'foo', + }); + + CredentialsLogic.actions.onApiTokenUpdateSuccess({ ...newToken, type: ADMIN }); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiTokenRawName: DEFAULT_VALUES.activeApiTokenRawName, + }); + }); + }); + + describe('showCredentialsForm', () => { + it('should reset to the default value, which closes the credentials form', () => { + mount({ + showCredentialsForm: true, + }); + + CredentialsLogic.actions.onApiTokenUpdateSuccess({ ...newToken, type: ADMIN }); + expect(CredentialsLogic.values).toEqual({ + ...values, + showCredentialsForm: false, + }); + }); + }); + }); + + describe('setCredentialsData', () => { + const meta = { + page: { + current: 1, + size: 1, + total_pages: 1, + total_results: 1, + }, + }; + + const values = { + ...DEFAULT_VALUES, + apiTokens: expect.any(Array), + meta: expect.any(Object), + isCredentialsDataComplete: expect.any(Boolean), + }; + + describe('apiTokens', () => { + it('should be set', () => { + mount(); + + CredentialsLogic.actions.setCredentialsData(meta, [newToken, newToken]); + expect(CredentialsLogic.values).toEqual({ + ...values, + apiTokens: [newToken, newToken], + }); + }); + }); + + describe('meta', () => { + it('should be set', () => { + mount(); + + CredentialsLogic.actions.setCredentialsData(meta, [newToken, newToken]); + expect(CredentialsLogic.values).toEqual({ + ...values, + meta, + }); + }); + }); + + describe('isCredentialsDataComplete', () => { + it('should be set to true so we know that data fetching has completed', () => { + mount({ + isCredentialsDataComplete: false, + }); + + CredentialsLogic.actions.setCredentialsData(meta, [newToken, newToken]); + expect(CredentialsLogic.values).toEqual({ + ...values, + isCredentialsDataComplete: true, + }); + }); + }); + }); + + describe('setCredentialsDetails', () => { + const values = { + ...DEFAULT_VALUES, + engines: expect.any(Array), + isCredentialsDetailsComplete: expect.any(Boolean), + }; + + describe('isCredentialsDataComplete', () => { + it('should be set to true so that we know data fetching has been completed', () => { + mount({ + isCredentialsDetailsComplete: false, + }); + + CredentialsLogic.actions.setCredentialsDetails(credentialsDetails); + expect(CredentialsLogic.values).toEqual({ + ...values, + isCredentialsDetailsComplete: true, + }); + }); + }); + + describe('engines', () => { + it('should set `engines` from the provided details object', () => { + mount({ + engines: [], + }); + + CredentialsLogic.actions.setCredentialsDetails(credentialsDetails); + expect(CredentialsLogic.values).toEqual({ + ...values, + engines: credentialsDetails.engines, + }); + }); + }); + }); + + describe('setNameInputBlurred', () => { + const values = { + ...DEFAULT_VALUES, + nameInputBlurred: expect.any(Boolean), + }; + + describe('nameInputBlurred', () => { + it('should set this value', () => { + mount({ + nameInputBlurred: false, + }); + + CredentialsLogic.actions.setNameInputBlurred(true); + expect(CredentialsLogic.values).toEqual({ + ...values, + nameInputBlurred: true, + }); + }); + }); + }); + + describe('setTokenReadWrite', () => { + const values = { + ...DEFAULT_VALUES, + activeApiToken: expect.any(Object), + }; + + describe('activeApiToken', () => { + it('should set "read" or "write" values', () => { + mount({ + activeApiToken: { + ...newToken, + read: false, + }, + }); + + CredentialsLogic.actions.setTokenReadWrite({ name: 'read', checked: true }); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...newToken, + read: true, + }, + }); + }); + }); + }); + + describe('setTokenName', () => { + const values = { + ...DEFAULT_VALUES, + activeApiToken: expect.any(Object), + activeApiTokenRawName: expect.any(String), + }; + + describe('activeApiToken', () => { + it('update the name property on the activeApiToken, formatted correctly', () => { + mount({ + activeApiToken: { + ...newToken, + name: 'bar', + }, + }); + + CredentialsLogic.actions.setTokenName('New Name'); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { ...newToken, name: 'new-name' }, + }); + }); + }); + + describe('activeApiTokenRawName', () => { + it('updates the raw name, with no formatting applied', () => { + mount(); + + CredentialsLogic.actions.setTokenName('New Name'); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiTokenRawName: 'New Name', + }); + }); + }); + }); + + describe('setTokenType', () => { + const values = { + ...DEFAULT_VALUES, + activeApiToken: { + ...newToken, + type: expect.any(String), + read: expect.any(Boolean), + write: expect.any(Boolean), + access_all_engines: expect.any(Boolean), + engines: expect.any(Array), + }, + }; + + describe('activeApiToken.access_all_engines', () => { + describe('when value is ADMIN', () => { + it('updates access_all_engines to false', () => { + mount({ + activeApiToken: { + ...newToken, + access_all_engines: true, + }, + }); + + CredentialsLogic.actions.setTokenType(ADMIN); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...values.activeApiToken, + access_all_engines: false, + }, + }); + }); + }); + + describe('when value is not ADMIN', () => { + it('will maintain access_all_engines value when true', () => { + mount({ + activeApiToken: { + ...newToken, + access_all_engines: true, + }, + }); + + CredentialsLogic.actions.setTokenType(PRIVATE); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...values.activeApiToken, + access_all_engines: true, + }, + }); + }); + + it('will maintain access_all_engines value when false', () => { + mount({ + activeApiToken: { + ...newToken, + access_all_engines: false, + }, + }); + + CredentialsLogic.actions.setTokenType(PRIVATE); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...values.activeApiToken, + access_all_engines: false, + }, + }); + }); + }); + }); + + describe('activeApiToken.engines', () => { + describe('when value is ADMIN', () => { + it('clears the array', () => { + mount({ + activeApiToken: { + ...newToken, + engines: [{}, {}], + }, + }); + + CredentialsLogic.actions.setTokenType(ADMIN); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...values.activeApiToken, + engines: [], + }, + }); + }); + }); + + describe('when value is not ADMIN', () => { + it('will maintain engines array', () => { + mount({ + activeApiToken: { + ...newToken, + engines: [{}, {}], + }, + }); + + CredentialsLogic.actions.setTokenType(PRIVATE); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...values.activeApiToken, + engines: [{}, {}], + }, + }); + }); + }); + }); + + describe('activeApiToken.write', () => { + describe('when value is PRIVATE', () => { + it('sets this to true', () => { + mount({ + activeApiToken: { + ...newToken, + write: false, + }, + }); + + CredentialsLogic.actions.setTokenType(PRIVATE); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...values.activeApiToken, + write: true, + }, + }); + }); + }); + + describe('when value is not PRIVATE', () => { + it('sets this to false', () => { + mount({ + activeApiToken: { + ...newToken, + write: true, + }, + }); + + CredentialsLogic.actions.setTokenType(ADMIN); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...values.activeApiToken, + write: false, + }, + }); + }); + }); + }); + + describe('activeApiToken.read', () => { + describe('when value is PRIVATE', () => { + it('sets this to true', () => { + mount({ + activeApiToken: { + ...newToken, + read: false, + }, + }); + + CredentialsLogic.actions.setTokenType(PRIVATE); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...values.activeApiToken, + read: true, + }, + }); + }); + }); + + describe('when value is not PRIVATE', () => { + it('sets this to false', () => { + mount({ + activeApiToken: { + ...newToken, + read: true, + }, + }); + + CredentialsLogic.actions.setTokenType(ADMIN); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...values.activeApiToken, + read: false, + }, + }); + }); + }); + }); + + describe('activeApiToken.type', () => { + it('sets the type value', () => { + mount({ + activeApiToken: { + ...newToken, + type: ADMIN, + }, + }); + + CredentialsLogic.actions.setTokenType(PRIVATE); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...values.activeApiToken, + type: PRIVATE, + }, + }); + }); + }); + }); + + describe('toggleCredentialsForm', () => { + const values = { + ...DEFAULT_VALUES, + activeApiTokenIsExisting: expect.any(Boolean), + activeApiToken: expect.any(Object), + activeApiTokenRawName: expect.any(String), + formErrors: expect.any(Array), + showCredentialsForm: expect.any(Boolean), + }; + + describe('showCredentialsForm', () => { + it('should toggle `showCredentialsForm`', () => { + mount({ + showCredentialsForm: false, + }); + + CredentialsLogic.actions.toggleCredentialsForm(); + expect(CredentialsLogic.values).toEqual({ + ...values, + showCredentialsForm: true, + }); + + CredentialsLogic.actions.toggleCredentialsForm(); + expect(CredentialsLogic.values).toEqual({ + ...values, + showCredentialsForm: false, + }); + }); + }); + + describe('formErrors', () => { + it('should reset `formErrors`', () => { + mount({ + formErrors: ['I am an error'], + }); + + CredentialsLogic.actions.toggleCredentialsForm(); + expect(CredentialsLogic.values).toEqual({ + ...values, + formErrors: [], + }); + }); + }); + + describe('activeApiTokenRawName', () => { + it('should set `activeApiTokenRawName` to the name of the provided token', () => { + mount(); + + CredentialsLogic.actions.toggleCredentialsForm(newToken); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiTokenRawName: 'myToken', + }); + }); + + it('should set `activeApiTokenRawName` to the default value if no token is provided', () => { + mount(); + + CredentialsLogic.actions.toggleCredentialsForm(); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiTokenRawName: DEFAULT_VALUES.activeApiTokenRawName, + }); + }); + + // TODO: This fails, is this an issue? Instead of reseting back to the default value, it sets it to the previously + // used value... to be honest, this should probably just be a selector + // it('should set `activeApiTokenRawName` back to the default value if no token is provided', () => { + // mount(); + // CredentialsLogic.actions.toggleCredentialsForm(newToken); + // CredentialsLogic.actions.toggleCredentialsForm(); + // expect(CredentialsLogic.values).toEqual({ + // ...values, + // activeApiTokenRawName: DEFAULT_VALUES.activeApiTokenRawName, + // }); + // }); + }); + + describe('activeApiToken', () => { + it('should set `activeApiToken` to the provided token', () => { + mount(); + + CredentialsLogic.actions.toggleCredentialsForm(newToken); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: newToken, + }); + }); + + it('should set `activeApiToken` to the default value if no token is provided', () => { + mount({ + activeApiToken: newToken, + }); + + CredentialsLogic.actions.toggleCredentialsForm(); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: DEFAULT_VALUES.activeApiToken, + }); + }); + }); + + // TODO: This should probably just be a selector... + describe('activeApiTokenIsExisting', () => { + it('should set `activeApiTokenIsExisting` to true when the provided token has an id', () => { + mount({ + activeApiTokenIsExisting: false, + }); + + CredentialsLogic.actions.toggleCredentialsForm(newToken); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiTokenIsExisting: true, + }); + }); + + it('should set `activeApiTokenIsExisting` to false when the provided token has no id', () => { + mount({ + activeApiTokenIsExisting: true, + }); + const { id, ...newTokenWithoutId } = newToken; + + CredentialsLogic.actions.toggleCredentialsForm(newTokenWithoutId); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiTokenIsExisting: false, + }); + }); + + it('should set `activeApiTokenIsExisting` to false when no token is provided', () => { + mount({ + activeApiTokenIsExisting: true, + }); + + CredentialsLogic.actions.toggleCredentialsForm(); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiTokenIsExisting: false, + }); + }); + }); + }); + + describe('hideCredentialsForm', () => { + const values = { + ...DEFAULT_VALUES, + showCredentialsForm: expect.any(Boolean), + activeApiTokenRawName: expect.any(String), + }; + + describe('activeApiTokenRawName', () => { + it('resets this value', () => { + mount({ + activeApiTokenRawName: 'foo', + }); + + CredentialsLogic.actions.hideCredentialsForm(); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiTokenRawName: '', + }); + }); + }); + + describe('showCredentialsForm', () => { + it('resets this value', () => { + mount({ + showCredentialsForm: true, + }); + + CredentialsLogic.actions.hideCredentialsForm(); + expect(CredentialsLogic.values).toEqual({ + ...values, + showCredentialsForm: false, + }); + }); + }); + }); + + describe('resetCredentials', () => { + const values = { + ...DEFAULT_VALUES, + isCredentialsDetailsComplete: expect.any(Boolean), + isCredentialsDataComplete: expect.any(Boolean), + formErrors: expect.any(Array), + }; + + describe('isCredentialsDetailsComplete', () => { + it('should reset to false', () => { + mount({ + isCredentialsDetailsComplete: true, + }); + + CredentialsLogic.actions.resetCredentials(); + expect(CredentialsLogic.values).toEqual({ + ...values, + isCredentialsDetailsComplete: false, + }); + }); + }); + + describe('isCredentialsDataComplete', () => { + it('should reset to false', () => { + mount({ + isCredentialsDataComplete: true, + }); + + CredentialsLogic.actions.resetCredentials(); + expect(CredentialsLogic.values).toEqual({ + ...values, + isCredentialsDataComplete: false, + }); + }); + }); + + describe('formErrors', () => { + it('should reset', () => { + mount({ + formErrors: ['I am an error'], + }); + + CredentialsLogic.actions.resetCredentials(); + expect(CredentialsLogic.values).toEqual({ + ...values, + formErrors: [], + }); + }); + }); + }); + + describe('initializeCredentialsData', () => { + it('should call fetchCredentials and fetchDetails', () => { + mount(); + jest.spyOn(CredentialsLogic.actions, 'fetchCredentials').mockImplementationOnce(() => {}); + jest.spyOn(CredentialsLogic.actions, 'fetchDetails').mockImplementationOnce(() => {}); + + CredentialsLogic.actions.initializeCredentialsData(); + expect(CredentialsLogic.actions.fetchCredentials).toHaveBeenCalled(); + expect(CredentialsLogic.actions.fetchDetails).toHaveBeenCalled(); + }); + }); + + describe('fetchCredentials', () => { + const meta = { + page: { + current: 1, + size: 1, + total_pages: 1, + total_results: 1, + }, + }; + const results: object[] = []; + + it('will call an API endpoint and set the results with the `setCredentialsData` action', async () => { + mount(); + jest.spyOn(CredentialsLogic.actions, 'setCredentialsData').mockImplementationOnce(() => {}); + const promise = Promise.resolve({ meta, results }); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + CredentialsLogic.actions.fetchCredentials(2); + expect(HttpLogic.values.http.get).toHaveBeenCalledWith('/api/app_search/credentials', { + query: { + 'page[current]': 2, + }, + }); + await promise; + expect(CredentialsLogic.actions.setCredentialsData).toHaveBeenCalledWith(meta, results); + }); + + it('handles errors', async () => { + mount(); + const promise = Promise.reject('An error occured'); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + CredentialsLogic.actions.fetchCredentials(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); + } + }); + }); + + describe('fetchDetails', () => { + it('will call an API endpoint and set the results with the `setCredentialsDetails` action', async () => { + mount(); + jest + .spyOn(CredentialsLogic.actions, 'setCredentialsDetails') + .mockImplementationOnce(() => {}); + const promise = Promise.resolve(credentialsDetails); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + CredentialsLogic.actions.fetchDetails(); + expect(HttpLogic.values.http.get).toHaveBeenCalledWith('/api/app_search/credentials/details'); + await promise; + expect(CredentialsLogic.actions.setCredentialsDetails).toHaveBeenCalledWith( + credentialsDetails + ); + }); + + it('handles errors', async () => { + mount(); + const promise = Promise.reject('An error occured'); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + CredentialsLogic.actions.fetchDetails(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); + } + }); + }); + + describe('deleteApiKey', () => { + const tokenName = 'abc123'; + + it('will call an API endpoint and set the results with the `onApiKeyDelete` action', async () => { + mount(); + jest.spyOn(CredentialsLogic.actions, 'onApiKeyDelete').mockImplementationOnce(() => {}); + const promise = Promise.resolve(); + (HttpLogic.values.http.delete as jest.Mock).mockReturnValue(promise); + + CredentialsLogic.actions.deleteApiKey(tokenName); + expect(HttpLogic.values.http.delete).toHaveBeenCalledWith( + `/api/app_search/credentials/${tokenName}` + ); + await promise; + expect(CredentialsLogic.actions.onApiKeyDelete).toHaveBeenCalledWith(tokenName); + }); + + it('handles errors', async () => { + mount(); + const promise = Promise.reject('An error occured'); + (HttpLogic.values.http.delete as jest.Mock).mockReturnValue(promise); + + CredentialsLogic.actions.deleteApiKey(tokenName); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); + } + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts new file mode 100644 index 0000000000000..43f2731711823 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts @@ -0,0 +1,264 @@ +/* + * 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 { formatApiName } from '../../utils/format_api_name'; +import { ADMIN, PRIVATE } from './constants'; + +import { HttpLogic } from '../../../shared/http'; +import { IMeta } from '../../../../../common/types'; +import { flashAPIErrors } from '../../../shared/flash_messages'; +import { IEngine } from '../../types'; +import { IApiToken, ICredentialsDetails } from './types'; + +interface ITokenReadWrite { + name: 'read' | 'write'; + checked: boolean; +} + +const defaultApiToken: IApiToken = { + name: '', + type: PRIVATE, + read: true, + write: true, + access_all_engines: true, +}; + +// TODO CREATE_MESSAGE, UPDATE_MESSAGE, and DELETE_MESSAGE from ent-search + +export interface ICredentialsLogicActions { + addEngineName(engineName: string): string; + onApiKeyDelete(tokenName: string): string; + onApiTokenCreateSuccess(apiToken: IApiToken): IApiToken; + onApiTokenError(formErrors: string[]): string[]; + onApiTokenUpdateSuccess(apiToken: IApiToken): IApiToken; + removeEngineName(engineName: string): string; + setAccessAllEngines(accessAll: boolean): boolean; + setCredentialsData(meta: IMeta, apiTokens: IApiToken[]): { meta: IMeta; apiTokens: IApiToken[] }; + setCredentialsDetails(details: ICredentialsDetails): ICredentialsDetails; + setNameInputBlurred(isBlurred: boolean): boolean; + setTokenReadWrite(tokenReadWrite: ITokenReadWrite): ITokenReadWrite; + setTokenName(name: string): string; + setTokenType(tokenType: string): string; + toggleCredentialsForm(apiToken?: IApiToken): IApiToken; + hideCredentialsForm(): { value: boolean }; + resetCredentials(): { value: boolean }; + initializeCredentialsData(): { value: boolean }; + fetchCredentials(page?: number): number; + fetchDetails(): { value: boolean }; + deleteApiKey(tokenName: string): string; +} + +export interface ICredentialsLogicValues { + activeApiToken: IApiToken; + activeApiTokenIsExisting: boolean; + activeApiTokenRawName: string; + apiTokens: IApiToken[]; + dataLoading: boolean; + engines: IEngine[]; + formErrors: string[]; + isCredentialsDataComplete: boolean; + isCredentialsDetailsComplete: boolean; + fullEngineAccessChecked: boolean; + meta: Partial; + nameInputBlurred: boolean; + showCredentialsForm: boolean; +} + +export const CredentialsLogic = kea< + MakeLogicType +>({ + path: ['enterprise_search', 'app_search', 'credentials_logic'], + actions: () => ({ + addEngineName: (engineName) => engineName, + onApiKeyDelete: (tokenName) => tokenName, + onApiTokenCreateSuccess: (apiToken) => apiToken, + onApiTokenError: (formErrors) => formErrors, + onApiTokenUpdateSuccess: (apiToken) => apiToken, + removeEngineName: (engineName) => engineName, + setAccessAllEngines: (accessAll) => accessAll, + setCredentialsData: (meta, apiTokens) => ({ meta, apiTokens }), + setCredentialsDetails: (details) => details, + setNameInputBlurred: (nameInputBlurred) => nameInputBlurred, + setTokenReadWrite: ({ name, checked }) => ({ + name, + checked, + }), + setTokenName: (name) => name, + setTokenType: (tokenType) => tokenType, + toggleCredentialsForm: (apiToken = { ...defaultApiToken }) => apiToken, + hideCredentialsForm: false, + resetCredentials: false, + initializeCredentialsData: true, + fetchCredentials: (page) => page, + fetchDetails: true, + deleteApiKey: (tokenName) => tokenName, + }), + reducers: () => ({ + apiTokens: [ + [], + { + setCredentialsData: (_, { apiTokens }) => apiTokens, + onApiTokenCreateSuccess: (apiTokens, apiToken) => [...apiTokens, apiToken], + onApiTokenUpdateSuccess: (apiTokens, apiToken) => [ + ...apiTokens.filter((token) => token.name !== apiToken.name), + apiToken, + ], + onApiKeyDelete: (apiTokens, tokenName) => + apiTokens.filter((token) => token.name !== tokenName), + }, + ], + meta: [ + {}, + { + setCredentialsData: (_, { meta }) => meta, + }, + ], + isCredentialsDetailsComplete: [ + false, + { + setCredentialsDetails: () => true, + resetCredentials: () => false, + }, + ], + isCredentialsDataComplete: [ + false, + { + setCredentialsData: () => true, + resetCredentials: () => false, + }, + ], + engines: [ + [], + { + setCredentialsDetails: (_, { engines }) => engines, + }, + ], + nameInputBlurred: [ + false, + { + setNameInputBlurred: (_, nameInputBlurred) => nameInputBlurred, + }, + ], + activeApiToken: [ + defaultApiToken, + { + addEngineName: (activeApiToken, engineName) => ({ + ...activeApiToken, + engines: [...(activeApiToken.engines || []), engineName], + }), + removeEngineName: (activeApiToken, engineName) => ({ + ...activeApiToken, + engines: (activeApiToken.engines || []).filter((name) => name !== engineName), + }), + setAccessAllEngines: (activeApiToken, accessAll) => ({ + ...activeApiToken, + access_all_engines: accessAll, + engines: accessAll ? [] : activeApiToken.engines, + }), + onApiTokenCreateSuccess: () => defaultApiToken, + onApiTokenUpdateSuccess: () => defaultApiToken, + setTokenName: (activeApiToken, name) => ({ ...activeApiToken, name: formatApiName(name) }), + setTokenReadWrite: (activeApiToken, { name, checked }) => ({ + ...activeApiToken, + [name]: checked, + }), + setTokenType: (activeApiToken, tokenType) => ({ + ...activeApiToken, + access_all_engines: tokenType === ADMIN ? false : activeApiToken.access_all_engines, + engines: tokenType === ADMIN ? [] : activeApiToken.engines, + write: tokenType === PRIVATE, + read: tokenType === PRIVATE, + type: tokenType, + }), + toggleCredentialsForm: (_, activeApiToken) => activeApiToken, + }, + ], + activeApiTokenRawName: [ + '', + { + setTokenName: (_, activeApiTokenRawName) => activeApiTokenRawName, + toggleCredentialsForm: (activeApiTokenRawName, activeApiToken) => + activeApiToken.name || activeApiTokenRawName, + hideCredentialsForm: () => '', + onApiTokenCreateSuccess: () => '', + onApiTokenUpdateSuccess: () => '', + }, + ], + activeApiTokenIsExisting: [ + false, + { + toggleCredentialsForm: (_, activeApiToken) => !!activeApiToken.id, + }, + ], + showCredentialsForm: [ + false, + { + toggleCredentialsForm: (showCredentialsForm) => !showCredentialsForm, + hideCredentialsForm: () => false, + onApiTokenCreateSuccess: () => false, + onApiTokenUpdateSuccess: () => false, + }, + ], + formErrors: [ + [], + { + onApiTokenError: (_, formErrors) => formErrors, + onApiTokenCreateSuccess: () => [], + toggleCredentialsForm: () => [], + resetCredentials: () => [], + }, + ], + }), + selectors: ({ selectors }) => ({ + // TODO fullEngineAccessChecked from ent-search + dataLoading: [ + () => [selectors.isCredentialsDetailsComplete, selectors.isCredentialsDataComplete], + (isCredentialsDetailsComplete, isCredentialsDataComplete) => { + return isCredentialsDetailsComplete === false || isCredentialsDataComplete === false; + }, + ], + }), + listeners: ({ actions, values }) => ({ + initializeCredentialsData: () => { + actions.fetchCredentials(); + actions.fetchDetails(); + }, + fetchCredentials: async (page = 1) => { + try { + const { http } = HttpLogic.values; + const query = { 'page[current]': page }; + const response = await http.get('/api/app_search/credentials', { query }); + actions.setCredentialsData(response.meta, response.results); + } catch (e) { + flashAPIErrors(e); + } + }, + fetchDetails: async () => { + try { + const { http } = HttpLogic.values; + const response = await http.get('/api/app_search/credentials/details'); + + actions.setCredentialsDetails(response); + } catch (e) { + flashAPIErrors(e); + } + }, + deleteApiKey: async (tokenName) => { + try { + const { http } = HttpLogic.values; + await http.delete(`/api/app_search/credentials/${tokenName}`); + + actions.onApiKeyDelete(tokenName); + } catch (e) { + flashAPIErrors(e); + } + }, + // TODO onApiTokenChange from ent-search + // TODO onEngineSelect from ent-search + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts new file mode 100644 index 0000000000000..9b09bd13a9086 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts @@ -0,0 +1,22 @@ +/* + * 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 { IEngine } from '../../types'; + +export interface ICredentialsDetails { + engines: IEngine[]; +} + +export interface IApiToken { + access_all_engines?: boolean; + key?: string; + engines?: string[]; + id?: number; + name: string; + read?: boolean; + type: string; + write?: boolean; +} 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/app_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts index 3cabc1051c74a..568a0a3365982 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts @@ -6,3 +6,10 @@ export * from '../../../common/types/app_search'; export { IRole, TRole, TAbility } from './utils/role'; + +export interface IEngine { + name: string; + type: string; + language: string; + result_fields: object[]; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/format_api_name/index.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/format_api_name/index.test.ts new file mode 100644 index 0000000000000..352ff237e4f08 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/format_api_name/index.test.ts @@ -0,0 +1,25 @@ +/* + * 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 { formatApiName } from '.'; + +describe('formatApiName', () => { + it('replaces non-alphanumeric characters with dashes', () => { + expect(formatApiName('f1 &&o$ 1 2 *&%da')).toEqual('f1-o-1-2-da'); + }); + + it('strips leading and trailing non-alphanumeric characters', () => { + expect(formatApiName('$$hello world**')).toEqual('hello-world'); + }); + + it('strips leading and trailing whitespace', () => { + expect(formatApiName(' test ')).toEqual('test'); + }); + + it('lowercases text', () => { + expect(formatApiName('SomeName')).toEqual('somename'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/format_api_name/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/format_api_name/index.ts new file mode 100644 index 0000000000000..cd1b1cfe15637 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/format_api_name/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const formatApiName = (rawName: string) => + rawName + .trim() + .replace(/[^a-zA-Z0-9]+/g, '-') // Replace all special/non-alphanumerical characters with dashes + .replace(/^[-]+|[-]+$/g, '') // Strip all leading and trailing dashes + .toLowerCase(); 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/enterprise_search/server/routes/app_search/credentials.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts index 000e6d63b5999..6b5f4a05b3aa6 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts @@ -25,41 +25,6 @@ describe('credentials routes', () => { it('creates a request handler', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ path: '/as/credentials/collection', - hasValidData: expect.any(Function), - }); - }); - - describe('hasValidData', () => { - it('should correctly validate that a response has data', () => { - const response = { - meta: { - page: { - current: 1, - total_pages: 1, - total_results: 1, - size: 25, - }, - }, - results: [ - { - id: 'loco_moco_account_id:5f3575de2b76ff13405f3155|name:asdfasdf', - key: 'search-fe49u2z8d5gvf9s4ekda2ad4', - name: 'asdfasdf', - type: 'search', - access_all_engines: true, - }, - ], - }; - - expect(mockRequestHandler.hasValidData(response)).toBe(true); - }); - - it('should correctly validate that a response does not have data', () => { - const response = { - foo: 'bar', - }; - - expect(mockRequestHandler.hasValidData(response)).toBe(false); }); }); @@ -75,4 +40,52 @@ describe('credentials routes', () => { }); }); }); + + describe('GET /api/app_search/credentials/details', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ method: 'get', payload: 'query' }); + + registerCredentialsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/credentials/details', + }); + }); + }); + + describe('DELETE /api/app_search/credentials/{name}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ method: 'delete', payload: 'params' }); + + registerCredentialsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + const mockRequest = { + params: { + name: 'abc123', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/credentials/abc123', + }); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts index 432f54c8e5b1c..0f2c1133192c5 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts @@ -8,25 +8,6 @@ import { schema } from '@kbn/config-schema'; import { IRouteDependencies } from '../../plugin'; -interface ICredential { - id: string; - key: string; - name: string; - type: string; - access_all_engines: boolean; -} -interface ICredentialsResponse { - results: ICredential[]; - meta?: { - page?: { - current: number; - total_results: number; - total_pages: number; - size: number; - }; - }; -} - export function registerCredentialsRoutes({ router, enterpriseSearchRequestHandler, @@ -42,9 +23,30 @@ export function registerCredentialsRoutes({ }, enterpriseSearchRequestHandler.createRequest({ path: '/as/credentials/collection', - hasValidData: (body?: ICredentialsResponse) => { - return Array.isArray(body?.results) && typeof body?.meta?.page?.total_results === 'number'; - }, }) ); + router.get( + { + path: '/api/app_search/credentials/details', + validate: false, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/credentials/details', + }) + ); + router.delete( + { + path: '/api/app_search/credentials/{name}', + validate: { + params: schema.object({ + name: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/as/credentials/${request.params.name}`, + })(context, request, response); + } + ); } 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/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/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/apm_api_integration/trial/tests/csm/js_errors.ts b/x-pack/test/apm_api_integration/trial/tests/csm/js_errors.ts new file mode 100644 index 0000000000000..0edffe7999a65 --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/csm/js_errors.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 expect from '@kbn/expect'; +import { expectSnapshot } from '../../../common/match_snapshot'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function rumJsErrorsApiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('CSM js errors', () => { + describe('when there is no data', () => { + it('returns no js errors', async () => { + const response = await supertest.get( + '/api/apm/rum-client/js-errors?pageSize=5&pageIndex=0&start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D' + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatchInline(` + Object { + "totalErrorGroups": 0, + "totalErrorPages": 0, + "totalErrors": 0, + } + `); + }); + }); + + describe('when there is data', () => { + before(async () => { + await esArchiver.load('8.0.0'); + await esArchiver.load('rum_8.0.0'); + }); + after(async () => { + await esArchiver.unload('8.0.0'); + await esArchiver.unload('rum_8.0.0'); + }); + + it('returns js errors', async () => { + const response = await supertest.get( + '/api/apm/rum-client/js-errors?pageSize=5&pageIndex=0&start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatchInline(` + Object { + "items": Array [], + "totalErrorGroups": 0, + "totalErrorPages": 0, + "totalErrors": 0, + } + `); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts index 69e54ea33c559..a6a031def34ea 100644 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ b/x-pack/test/apm_api_integration/trial/tests/index.ts @@ -37,6 +37,7 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr loadTestFile(require.resolve('./csm/long_task_metrics.ts')); loadTestFile(require.resolve('./csm/url_search.ts')); loadTestFile(require.resolve('./csm/page_views.ts')); + loadTestFile(require.resolve('./csm/js_errors.ts')); }); }); } 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=