diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 745101711171c..99bd6d6b0757d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1100,6 +1100,32 @@ src/plugins/discover/public/context_awareness/profile_providers/security @elasti /x-pack/test/screenshot_creation @elastic/platform-docs # Visualizations +/test/functional/apps/getting_started/*.ts @elastic/kibana-visualizations # Assigned per https://github.com/elastic/kibana/pull/199767#discussion_r1840485031 +/x-pack/test/upgrade/apps/graph @elastic/kibana-visualizations +/x-pack/test/functional/page_objects/log_wrapper.ts @elastic/kibana-visualizations # Assigned per https://github.com/elastic/kibana/pull/36437 +/x-pack/test/functional/page_objects/graph_page.ts @elastic/kibana-visualizations +/x-pack/test/functional/apps/managed_content @elastic/kibana-visualizations +/test/functional/services/visualizations @elastic/kibana-visualizations +/test/functional/services/renderable.ts @elastic/kibana-visualizations +/test/functional/page_objects/vega_chart_page.ts @elastic/kibana-visualizations +/test/functional/page_objects/visual_builder_page.ts @elastic/kibana-visualizations +/test/functional/page_objects/visualize_*.ts @elastic/kibana-visualizations +/test/functional/page_objects/timelion_page.ts @elastic/kibana-visualizations +/test/functional/page_objects/time_to_visualize_page.ts @elastic/kibana-visualizations +/test/functional/page_objects/tag_cloud_page.ts @elastic/kibana-visualizations +/x-pack/test/functional/page_objects/lens_page.ts @elastic/kibana-visualizations +/x-pack/test/functional/es_archives/lens @elastic/kibana-visualizations +/x-pack/test/examples/embedded_lens @elastic/kibana-visualizations +/x-pack/test/api_integration/fixtures/kbn_archiver/lens/constant_keyword.json @elastic/kibana-visualizations +/test/plugin_functional/test_suites/custom_visualizations @elastic/kibana-visualizations +/test/plugin_functional/plugins/kbn_tp_custom_visualizations @elastic/kibana-visualizations +/x-pack/test/functional/fixtures/kbn_archiver/visualize @elastic/kibana-visualizations +/x-pack/test/functional/fixtures/kbn_archiver/lens @elastic/kibana-visualizations +/test/functional/page_objects/legacy/data_table_vis.ts @elastic/kibana-visualizations +/test/functional/page_objects/annotation_library_editor_page.ts @elastic/kibana-visualizations # Assigned per git blame +/test/functional/firefox/visualize.config.ts @elastic/kibana-visualizations +/test/examples/expressions_explorer/*.ts @elastic/kibana-visualizations +/test/accessibility/apps/visualize.ts @elastic/kibana-visualizations /x-pack/test/accessibility/apps/group3/graph.ts @elastic/kibana-visualizations /x-pack/test/accessibility/apps/group2/lens.ts @elastic/kibana-visualizations /x-pack/test/functional/apps/visualize @elastic/kibana-visualizations @@ -1113,6 +1139,10 @@ src/plugins/discover/public/context_awareness/profile_providers/security @elasti /x-pack/test_serverless/functional/fixtures/kbn_archiver/lens/ @elastic/kibana-visualizations packages/kbn-monaco/src/esql @elastic/kibana-esql +# ESQL +/test/api_integration/apis/esql/*.ts @elastic/kibana-esql +/test/functional/services/esql.ts @elastic/kibana-esql + # Global Experience ### Global Experience Reporting @@ -1248,9 +1278,10 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/test/accessibility/apps/group3/stack_monitoring.ts @elastic/stack-monitoring # Fleet -/x-pack/test_serverless/api_integration/test_suites/security/fleet @elastic/fleet -/x-pack/test/fleet_packages @elastic/fleet +/x-pack/test/api_integration/services/fleet_and_agents.ts @elastic/fleet /x-pack/test/fleet_api_integration @elastic/fleet +/x-pack/test/fleet_packages @elastic/fleet +/test/api_integration/apis/custom_integration/*.ts @elastic/fleet /x-pack/test/fleet_cypress @elastic/fleet /x-pack/test/fleet_functional @elastic/fleet /src/dev/build/tasks/bundle_fleet_packages.ts @elastic/fleet @elastic/kibana-operations @@ -1646,6 +1677,11 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib /x-pack/test/functional/es_archives/cases/signals/hosts_users @elastic/response-ops # Enterprise Search +# search +/x-pack/test/functional_solution_sidenav/tests/search_sidenav.ts @elastic/search-kibana +/x-pack/test/functional/services/search_sessions.ts @elastic/search-kibana +/x-pack/test/functional/page_objects/search_* @elastic/search-kibana +/x-pack/test/functional/apps/search_playground @elastic/search-kibana /x-pack/test_serverless/functional/page_objects/svl_ingest_pipelines.ts @elastic/search-kibana /x-pack/test/functional/apps/dev_tools/embedded_console.ts @elastic/search-kibana /x-pack/test/functional/apps/ingest_pipelines/feature_controls/ingest_pipelines_security.ts @elastic/search-kibana @@ -1662,18 +1698,51 @@ x-pack/test/api_integration/apis/management/index_management/inference_endpoints /x-pack/test/functional_search/ @elastic/search-kibana # Management Experience - Deployment Management -/x-pack/test/api_integration/services/index_management.ts @elastic/kibana-management -/x-pack/test/functional/apps/license_management @elastic/kibana-management +/x-pack/test/functional/services/ace_editor.js @elastic/kibana-management +/x-pack/test/functional/page_objects/remote_clusters_page.ts @elastic/kibana-management +/x-pack/test/stack_functional_integration/apps/ccs @elastic/kibana-management +/x-pack/test/functional/services/data_stream.ts @elastic/kibana-management +/x-pack/test/functional/page_objects/watcher_page.ts @elastic/kibana-management +/x-pack/test/functional/page_objects/upgrade_assistant_page.ts @elastic/kibana-management +/x-pack/test/functional/page_objects/snapshot_restore_page.ts @elastic/kibana-management +/x-pack/test/functional/page_objects/rollup_page.ts @elastic/kibana-management +/x-pack/test/functional/page_objects/ingest_pipelines_page.ts @elastic/kibana-management +/x-pack/test/functional/page_objects/grok_debugger_page.ts @elastic/kibana-management +/x-pack/test/functional/page_objects/cross_cluster_replication_page.ts @elastic/kibana-management +/x-pack/test/functional/fixtures/ingest_pipeline_example_mapping.csv @elastic/kibana-management +/x-pack/test/functional/apps/snapshot_restore @elastic/kibana-management /x-pack/test/functional/apps/painless_lab @elastic/kibana-management +/x-pack/test_serverless/functional/test_suites/common/spaces/spaces_management.ts @elastic/kibana-management +/x-pack/test/stack_functional_integration/apps/management @elastic/kibana-management +/x-pack/test/functional/page_objects/*_management_page.ts @elastic/kibana-management +/test/functional/services/saved_query_management_component.ts @elastic/kibana-management +/test/functional/services/management @elastic/kibana-management +/x-pack/test/functional/apps/cross_cluster_replication @elastic/kibana-management +/x-pack/test/functional/apps/ingest_pipelines @elastic/kibana-management +/x-pack/test/functional/apps/home @elastic/kibana-management +/x-pack/test/functional/apps/license_management @elastic/kibana-management /x-pack/test/functional/apps/management @elastic/kibana-management +/x-pack/test/functional/apps/remote_clusters @elastic/kibana-management +/x-pack/test/functional/apps/upgrade_assistant @elastic/kibana-management +/x-pack/test/functional/apps/dev_tools @elastic/kibana-management +/test/plugin_functional/test_suites/management @elastic/kibana-management +/x-pack/test/upgrade_assistant_integration @elastic/kibana-management +/test/plugin_functional/plugins/management_test_plugin @elastic/kibana-management +/test/functional/page_objects/management/*.ts @elastic/kibana-management +/test/functional/page_objects/embedded_console.ts @elastic/kibana-management +/test/functional/page_objects/console_page.ts @elastic/kibana-management +/test/functional/firefox/console.config.ts @elastic/kibana-management +/test/functional/apps/management @elastic/kibana-management +/test/functional/apps/saved_objects_management @elastic/kibana-management +/test/functional/apps/console/*.ts @elastic/kibana-management +/test/api_integration/apis/console/*.ts @elastic/kibana-management +/test/accessibility/apps/management.ts @elastic/kibana-management +/test/accessibility/apps/console.ts @elastic/kibana-management /x-pack/test/api_integration/services/index_management.ts @elastic/kibana-management /x-pack/test/functional/services/grok_debugger.js @elastic/kibana-management -/x-pack/test/functional/apps/cross_cluster_replication @elastic/kibana-management /x-pack/test/functional/apps/grok_debugger @elastic/kibana-management /x-pack/test/functional/apps/index_lifecycle_management @elastic/kibana-management /x-pack/test/functional/apps/index_management @elastic/kibana-management -/x-pack/test/functional/apps/ingest_pipelines @elastic/kibana-management -/x-pack/test/functional/apps/upgrade_assistant @elastic/kibana-management /x-pack/test/api_integration/services/ingest_pipelines @elastic/kibana-management /x-pack/test/functional/apps/watcher @elastic/kibana-management /x-pack/test/api_integration/apis/watcher @elastic/kibana-management @@ -1714,9 +1783,12 @@ x-pack/test/api_integration/apis/management/index_management/inference_endpoints #CC# /x-pack/plugins/cross_cluster_replication/ @elastic/kibana-management # Security Solution +/x-pack/test/functional/es_archives/rule_exceptions @elastic/security-solution # Assigned per https://github.com/elastic/kibana/pull/199795/files/ae80bb252bc71f787c122849fcb9b01e386fc5e9#r1840233040 +/x-pack/test/functional_solution_sidenav/tests/security_sidenav.ts @elastic/security-solution +/x-pack/test/common/utils/security_solution/index.ts @elastic/security-solution +/x-pack/test/plugin_functional/plugins/resolver_test @elastic/security-solution /x-pack/test/common/services/security_solution @elastic/security-solution /x-pack/test/api_integration/services/security_solution_*.gen.ts @elastic/security-solution -/x-pack/test_serverless/functional/test_suites/security/index.feature_flags.ts @elastic/security-solution /x-pack/test/accessibility/apps/group3/security_solution.ts @elastic/security-solution /x-pack/test_serverless/functional/test_suites/security/config.ts @elastic/security-solution @elastic/appex-qa /x-pack/test_serverless/functional/test_suites/security/config.feature_flags.ts @elastic/security-solution @@ -2110,19 +2182,44 @@ x-pack/test/profiling_api_integration @elastic/obs-ux-infra_services-team x-pack/plugins/observability_solution/observability_shared/public/components/profiling @elastic/obs-ux-infra_services-team # Shared UX -/test/api_integration/apis/guided_onboarding @elastic/appex-sharedux +/test/functional/page_objects/files_management.ts @elastic/appex-sharedux # Assigned per https://github.com/elastic/kibana/pull/200017#discussion_r1840477291 +/test/accessibility/apps/home.ts @elastic/appex-sharedux # Assigned per https://github.com/elastic/kibana/pull/199771/files#r1840077237 +/test/api_integration/apis/home/*.ts @elastic/appex-sharedux # Assigned per https://github.com/elastic/kibana/pull/199771/files#r1840077065 +/test/functional/apps/home @elastic/appex-sharedux # Assigned per https://github.com/elastic/kibana/pull/199771/files#r1840075278 +/x-pack/test/plugin_functional/plugins/global_search_test @elastic/appex-sharedux +/test/functional/services/saved_objects_finder.ts @elastic/appex-sharedux +/test/functional/apps/kibana_overview @elastic/appex-sharedux +/x-pack/test/functional_solution_sidenav/config.ts @elastic/appex-sharedux +/x-pack/test/functional_solution_sidenav/ftr_provider_context.ts @elastic/appex-sharedux +/x-pack/test/functional_solution_sidenav/services.ts @elastic/appex-sharedux +/x-pack/test/functional_solution_sidenav/index.ts @elastic/appex-sharedux +/x-pack/test/functional_cloud @elastic/appex-sharedux +/x-pack/test/functional/services/user_menu.js @elastic/appex-sharedux # Assigned per https://github.com/elastic/kibana/pull/179270 +/x-pack/test/functional/page_objects/reporting_page.ts @elastic/appex-sharedux +/x-pack/test/accessibility/apps/**/config.ts @elastic/appex-sharedux +/x-pack/test/accessibility/apps/**/index.ts @elastic/appex-sharedux +/x-pack/test/upgrade/apps/reporting @elastic/appex-sharedux +/x-pack/test/functional/apps/advanced_settings @elastic/appex-sharedux +/test/functional/services/monaco_editor.ts @elastic/appex-sharedux +/x-pack/test/functional/fixtures/kbn_archiver/global_search @elastic/appex-sharedux +/x-pack/test/plugin_functional/test_suites/global_search @elastic/appex-sharedux /test/plugin_functional/test_suites/shared_ux @elastic/appex-sharedux -/x-pack/test/functional/apps/advanced_settings @elastic/appex-sharedux @elastic/kibana-management -/test/examples/content_management @elastic/appex-sharedux /test/plugin_functional/plugins/kbn_sample_panel_action @elastic/appex-sharedux -/test/functional/apps/kibana_overview @elastic/appex-sharedux /test/plugin_functional/plugins/eui_provider_dev_warning @elastic/appex-sharedux +/test/functional/page_objects/settings_page.ts @elastic/appex-sharedux +/test/functional/apps/sharing/*.ts @elastic/appex-sharedux +/test/functional/apps/kibana_overviews @elastic/appex-sharedux +/test/examples/ui_actions/*.ts @elastic/appex-sharedux +/test/examples/state_sync/*.ts @elastic/appex-sharedux +/test/examples/error_boundary/index.ts @elastic/appex-sharedux +/test/examples/content_management/*.ts @elastic/appex-sharedux +/test/examples/bfetch_explorer/*.ts @elastic/appex-sharedux +/test/api_integration/apis/guided_onboarding @elastic/appex-sharedux /x-pack/test/banners_functional @elastic/appex-sharedux /x-pack/test/custom_branding @elastic/appex-sharedux -/x-pack/test/accessibility/apps/group3/snapshot_and_restore.ts @elastic/appex-sharedux -/x-pack/test/accessibility/apps/group3/tags.ts @elastic/appex-sharedux /x-pack/test/api_integration/apis/content_management @elastic/appex-sharedux -/x-pack/test/functional_solution_sidenav @elastic/appex-sharedux +/x-pack/test/accessibility/apps/group3/tags.ts @elastic/appex-sharedux +/x-pack/test/accessibility/apps/group3/snapshot_and_restore.ts @elastic/appex-sharedux /x-pack/test_serverless/functional/test_suites/common/spaces/spaces_selection.ts @elastic/appex-sharedux /x-pack/test_serverless/functional/test_suites/common/spaces/index.ts @elastic/appex-sharedux packages/react @elastic/appex-sharedux diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 0c9bd6ada3904..85c5bcfbf1127 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -93,37 +93,36 @@ The 8.16.0 release includes the following known issues. [discrete] [[known-199902]] -.Stack Monitoring shows "Unable to load page error" +.Stack Monitoring shows "Unable to load page" error [%collapsible] ==== *Details* + -The Stack Monitoring pages Overview, Nodes, Logs can stop working with the error "Unable to load page error". The Stack trace mentions `TypeError: Cannot read properties of undefined (reading 'logsLocator')`. +The Overview, Nodes, and Logs pages in Stack Monitoring show an "Unable to load page" error. The Stack trace mentions `TypeError: Cannot read properties of undefined (reading 'logsLocator')`. *Workaround* + -Disabling the `Set feature visibility > Logs` feature at Kibana Space settings level will prevent the error to occur. Please note the `Logs` feature will not be available on such space. +Disabling the `Set feature visibility > Logs` feature at the Kibana Space settings level will prevent the error from occurring. Please note the `Logs` feature will not be available on those spaces. -It's also possible to `Observability > Logs` feature privilege to `None` on the role level - this will hide the `Logs` feature for individual users and prevent the error for these users as well. +It's also possible to set the `Observability > Logs` feature privilege to `None` at the role level. This will hide the `Logs` feature from individual users and prevent the error for these users as well. For more information, refer to {kibana-issue}199902[#199902]. ==== [discrete] [[known-199891-199892]] -.Onboarding, tutorial of APM and OpenTelemetry and some "Beats Only" integrations will show the error "Unable to load page error" +.Onboarding, tutorial of APM and OpenTelemetry and some "Beats Only" integrations shows "Unable to load page" error [%collapsible] ==== *Details* + -Tutorials linked from the {kib} home page show an error "Unable to load page error". The Stack trace mentions `The above error occurred in tutorial_TutorialUi`. +Tutorials linked from the {kib} home page show an "Unable to load page" error. The Stack trace mentions `The above error occurred in tutorial_TutorialUi`. *Workaround* + -The APM / OpenTelemetry tutorials represented a shortcut to get important parameters to use in the configuration files quickly. -It is still possible to obtain the same parameters following the documentation tutorials of APM. +The APM / OpenTelemetry tutorials represented a shortcut to quickly add important parameters to the configuration files. +It is still possible to obtain the same parameters following the tutorials in the APM documentation. More information can be found in the {observability-guide}/apm-collect-application-data.html[APM documentation] and the {observability-guide}/get-started-with-fleet-apm-server.html[Fleet documentation]. For information about how to create APM API keys, please check the {observability-guide}/apm-api-key.html#apm-create-an-api-key[API key documentation]. - For more information, refer to {kibana-issue}199891[#199891] and {kibana-issue}199892[#199892]. ==== @@ -156,9 +155,22 @@ The Observability AI Assistant specific advanced setting for Logs index patterns //*Impact* + //!!TODO!! ==== - - +[float] +[[breaking-changes-8.16.0]] +=== Breaking changes + +Breaking changes can prevent your application from optimal operation and performance. +Before you upgrade to 8.16.0, review the breaking changes, then mitigate the impact to your application. + +[discrete] +.Updated request processing during shutdown. +[%collapsible] +==== +*Details* + +During shutdown, {kib} now waits for all the ongoing requests to complete according to the `server.shutdownTimeout` setting. During that period, the incoming socket is closed and any new incoming requests are rejected. Before this update, new incoming requests received a response with the status code 503 and body `{"message": "Kibana is shutting down and not accepting new incoming requests"}`. For more information, refer to {kibana-pull}180986[#180986]. +==== + [float] [[features-8.16.0]] === Features diff --git a/docs/upgrade-notes.asciidoc b/docs/upgrade-notes.asciidoc index a0c2d6c1afccb..c2d866f90eed3 100644 --- a/docs/upgrade-notes.asciidoc +++ b/docs/upgrade-notes.asciidoc @@ -434,6 +434,15 @@ The endpoint is now split into two separate endpoints: // General settings +[discrete] +[[breaking-180986]] +.[General settings] Updated request processing during shutdown. (8.16) +[%collapsible] +==== +*Details* + +During shutdown, {kib} now waits for all the ongoing requests to complete according to the `server.shutdownTimeout` setting. During that period, the incoming socket is closed and any new incoming requests are rejected. Before this update, new incoming requests received a response with the status code 503 and body `{"message": "Kibana is shutting down and not accepting new incoming requests"}`. For more information, refer to {kibana-pull}180986[#180986]. +==== + [discrete] [[breaking-111535]] .[General settings] Removed `CONFIG_PATH` and `DATA_PATH` environment variables. (8.0) diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 0fde92c1fa3f9..d30ac3c4552e2 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -1304,14 +1304,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -2083,14 +2086,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -2484,14 +2490,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -3253,14 +3262,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -3647,14 +3659,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -5094,14 +5109,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -21317,7 +21335,7 @@ ] } }, - "/api/fleet/epm/packages/{pkgName}/{pkgVersion}/{filePath*}": { + "/api/fleet/epm/packages/{pkgName}/{pkgVersion}/{filePath}": { "get": { "operationId": "get-fleet-epm-packages-pkgname-pkgversion-filepath", "parameters": [ diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index 229bda84b8629..4b56e3581c66f 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -1304,14 +1304,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -2083,14 +2086,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -2484,14 +2490,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -3253,14 +3262,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -3647,14 +3659,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -5094,14 +5109,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -21317,7 +21335,7 @@ ] } }, - "/api/fleet/epm/packages/{pkgName}/{pkgVersion}/{filePath*}": { + "/api/fleet/epm/packages/{pkgName}/{pkgVersion}/{filePath}": { "get": { "operationId": "get-fleet-epm-packages-pkgname-pkgversion-filepath", "parameters": [ diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 9f0c38baded7d..55dd5277d1d93 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -1018,14 +1018,17 @@ paths: - last_execution_date flapping: additionalProperties: false + description: When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: The minimum number of runs in which the threshold must be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: The minimum number of times an alert must switch states in the look back window. maximum: 20 minimum: 2 type: number @@ -1600,14 +1603,17 @@ paths: type: boolean flapping: additionalProperties: false + description: When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: The minimum number of runs in which the threshold must be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: The minimum number of times an alert must switch states in the look back window. maximum: 20 minimum: 2 type: number @@ -1925,14 +1931,17 @@ paths: - last_execution_date flapping: additionalProperties: false + description: When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: The minimum number of runs in which the threshold must be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: The minimum number of times an alert must switch states in the look back window. maximum: 20 minimum: 2 type: number @@ -2510,14 +2519,17 @@ paths: - active flapping: additionalProperties: false + description: When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: The minimum number of runs in which the threshold must be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: The minimum number of times an alert must switch states in the look back window. maximum: 20 minimum: 2 type: number @@ -2807,14 +2819,17 @@ paths: - last_execution_date flapping: additionalProperties: false + description: When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: The minimum number of runs in which the threshold must be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: The minimum number of times an alert must switch states in the look back window. maximum: 20 minimum: 2 type: number @@ -3852,14 +3867,17 @@ paths: - last_execution_date flapping: additionalProperties: false + description: When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: The minimum number of runs in which the threshold must be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: The minimum number of times an alert must switch states in the look back window. maximum: 20 minimum: 2 type: number @@ -19305,7 +19323,7 @@ paths: tags: - Elastic Package Manager (EPM) x-beta: true - /api/fleet/epm/packages/{pkgName}/{pkgVersion}/{filePath*}: + /api/fleet/epm/packages/{pkgName}/{pkgVersion}/{filePath}: get: operationId: get-fleet-epm-packages-pkgname-pkgversion-filepath parameters: diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 50fd92fdc8a9c..b2c3ae00be9d0 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -1367,14 +1367,17 @@ paths: - last_execution_date flapping: additionalProperties: false + description: When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: The minimum number of runs in which the threshold must be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: The minimum number of times an alert must switch states in the look back window. maximum: 20 minimum: 2 type: number @@ -1948,14 +1951,17 @@ paths: type: boolean flapping: additionalProperties: false + description: When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: The minimum number of runs in which the threshold must be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: The minimum number of times an alert must switch states in the look back window. maximum: 20 minimum: 2 type: number @@ -2273,14 +2279,17 @@ paths: - last_execution_date flapping: additionalProperties: false + description: When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: The minimum number of runs in which the threshold must be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: The minimum number of times an alert must switch states in the look back window. maximum: 20 minimum: 2 type: number @@ -2857,14 +2866,17 @@ paths: - active flapping: additionalProperties: false + description: When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: The minimum number of runs in which the threshold must be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: The minimum number of times an alert must switch states in the look back window. maximum: 20 minimum: 2 type: number @@ -3154,14 +3166,17 @@ paths: - last_execution_date flapping: additionalProperties: false + description: When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: The minimum number of runs in which the threshold must be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: The minimum number of times an alert must switch states in the look back window. maximum: 20 minimum: 2 type: number @@ -4191,14 +4206,17 @@ paths: - last_execution_date flapping: additionalProperties: false + description: When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: The minimum number of runs in which the threshold must be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: The minimum number of times an alert must switch states in the look back window. maximum: 20 minimum: 2 type: number @@ -6646,6 +6664,41 @@ paths: summary: Push a case to an external service tags: - cases + /api/cases/{caseId}/files: + post: + description: | + Attach a file to a case. You must have `all` privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're updating. The request must include: + - The `Content-Type: multipart/form-data` HTTP header. + - The location of the file that is being uploaded. + operationId: addCaseFileDefaultSpace + parameters: + - $ref: '#/components/parameters/Cases_kbn_xsrf' + - $ref: '#/components/parameters/Cases_case_id' + requestBody: + content: + multipart/form-data; Elastic-Api-Version=2023-10-31: + schema: + $ref: '#/components/schemas/Cases_add_case_file_request' + required: true + responses: + '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + examples: + addCaseFileResponse: + $ref: '#/components/examples/Cases_add_comment_response' + schema: + $ref: '#/components/schemas/Cases_case_response_properties' + description: Indicates a successful call. + '401': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + $ref: '#/components/schemas/Cases_4xx_response' + description: Authorization information is missing or invalid. + summary: Attach a file to a case + tags: + - cases /api/cases/{caseId}/user_actions: get: deprecated: true @@ -22074,7 +22127,7 @@ paths: summary: Update package settings tags: - Elastic Package Manager (EPM) - /api/fleet/epm/packages/{pkgName}/{pkgVersion}/{filePath*}: + /api/fleet/epm/packages/{pkgName}/{pkgVersion}/{filePath}: get: operationId: get-fleet-epm-packages-pkgname-pkgversion-filepath parameters: @@ -43614,6 +43667,20 @@ components: - $ref: '#/components/schemas/Cases_add_alert_comment_request_properties' - $ref: '#/components/schemas/Cases_add_user_comment_request_properties' title: Add case comment request + Cases_add_case_file_request: + description: Defines the file that will be attached to the case. Optional parameters will be generated automatically from the file metadata if not defined. + type: object + properties: + file: + description: The file being attached to the case. + format: binary + type: string + filename: + description: The desired name of the file being attached to the case, it can be different than the name of the file in the filesystem. **This should not include the file extension.** + type: string + required: + - file + title: Add case file request properties Cases_add_user_comment_request_properties: description: Defines properties for case comment requests when type is user. properties: diff --git a/oas_docs/package-lock.json b/oas_docs/package-lock.json index 6527a6ee6a5dd..70fff86254f65 100644 --- a/oas_docs/package-lock.json +++ b/oas_docs/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@redocly/cli": "^1.25.7", + "@redocly/cli": "^1.25.11", "bump-cli": "^2.8.4" } }, @@ -518,6 +518,7 @@ "version": "1.25.11", "resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-1.25.11.tgz", "integrity": "sha512-dttBsmLnnbTlJCTa+s7Sy+qtXDq692n7Ru3nUUIHp9XdCbhXIHWhpc8uAl+GmR4MGbVe8ohATl3J+zX3aFy82A==", + "license": "MIT", "dependencies": { "@redocly/openapi-core": "1.25.11", "abort-controller": "^3.0.0", diff --git a/oas_docs/package.json b/oas_docs/package.json index d007a9881acad..3f6cae5c044b3 100644 --- a/oas_docs/package.json +++ b/oas_docs/package.json @@ -8,7 +8,7 @@ }, "dependencies": { "bump-cli": "^2.8.4", - "@redocly/cli": "^1.25.7" + "@redocly/cli": "^1.25.11" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/package.json b/package.json index fb6b8b093560c..acb1a118284ed 100644 --- a/package.json +++ b/package.json @@ -1022,7 +1022,7 @@ "@langchain/langgraph": "0.2.19", "@langchain/openai": "^0.3.11", "@langtrase/trace-attributes": "^3.0.8", - "@launchdarkly/node-server-sdk": "^9.7.0", + "@launchdarkly/node-server-sdk": "^9.7.1", "@launchdarkly/openfeature-node-server": "^1.0.0", "@loaders.gl/core": "^3.4.7", "@loaders.gl/json": "^3.4.7", @@ -1633,7 +1633,7 @@ "@types/resolve": "^1.20.1", "@types/scheduler": "^0.23.0", "@types/seedrandom": ">=2.0.0 <4.0.0", - "@types/selenium-webdriver": "^4.1.26", + "@types/selenium-webdriver": "^4.1.27", "@types/semver": "^7.5.8", "@types/set-value": "^2.0.0", "@types/sinon": "^7.0.13", @@ -1680,7 +1680,7 @@ "buildkite-test-collector": "^1.7.0", "callsites": "^3.1.0", "chance": "1.0.18", - "chromedriver": "^130.0.1", + "chromedriver": "^130.0.4", "clarify": "^2.2.0", "clean-webpack-plugin": "^3.0.0", "cli-progress": "^3.12.0", @@ -1731,7 +1731,7 @@ "file-loader": "^4.2.0", "find-cypress-specs": "^1.41.4", "form-data": "^4.0.0", - "geckodriver": "^4.5.1", + "geckodriver": "^5.0.0", "gulp-brotli": "^3.0.0", "gulp-postcss": "^9.0.1", "gulp-terser": "^2.1.0", @@ -1806,7 +1806,7 @@ "rxjs-marbles": "^7.0.1", "sass-embedded": "^1.78.0", "sass-loader": "^10.5.1", - "selenium-webdriver": "^4.25.0", + "selenium-webdriver": "^4.26.0", "sharp": "0.32.6", "simple-git": "^3.16.0", "sinon": "^7.4.2", diff --git a/packages/kbn-router-to-openapispec/src/__snapshots__/generate_oas.test.ts.snap b/packages/kbn-router-to-openapispec/src/__snapshots__/generate_oas.test.ts.snap index 1c1f5bed02a0e..ccf5fad20a71a 100644 --- a/packages/kbn-router-to-openapispec/src/__snapshots__/generate_oas.test.ts.snap +++ b/packages/kbn-router-to-openapispec/src/__snapshots__/generate_oas.test.ts.snap @@ -228,7 +228,7 @@ OK response oas-test-version-2", "x-discontinued": "route discontinued version or date", }, }, - "/foo/{id}/{path*}": Object { + "/foo/{id}/{path}": Object { "delete": Object { "description": "route description", "operationId": "delete-foo-id-path", @@ -570,7 +570,7 @@ OK response oas-test-version-2", ], }, }, - "/no-xsrf/{id}/{path*}": Object { + "/no-xsrf/{id}/{path}": Object { "post": Object { "deprecated": true, "operationId": "post-no-xsrf-id-path-2", diff --git a/packages/kbn-router-to-openapispec/src/generate_oas.test.fixture.ts b/packages/kbn-router-to-openapispec/src/generate_oas.test.fixture.ts index f4ba66f992134..dedba5036d7ef 100644 --- a/packages/kbn-router-to-openapispec/src/generate_oas.test.fixture.ts +++ b/packages/kbn-router-to-openapispec/src/generate_oas.test.fixture.ts @@ -151,7 +151,7 @@ export const sharedOas = { tags: ['versioned'], }, }, - '/foo/{id}/{path*}': { + '/foo/{id}/{path}': { get: { description: 'route description', operationId: 'get-foo-id-path', diff --git a/packages/kbn-router-to-openapispec/src/generate_oas.test.ts b/packages/kbn-router-to-openapispec/src/generate_oas.test.ts index 25b786ac7c2c7..76a4f560006b3 100644 --- a/packages/kbn-router-to-openapispec/src/generate_oas.test.ts +++ b/packages/kbn-router-to-openapispec/src/generate_oas.test.ts @@ -314,9 +314,9 @@ describe('generateOpenApiDocument', () => { } ); // router paths - expect(result.paths['/1-1/{id}/{path*}']!.get!.tags).toEqual(['1', '2']); - expect(result.paths['/1-2/{id}/{path*}']!.get!.tags).toEqual(['1']); - expect(result.paths['/2-1/{id}/{path*}']!.get!.tags).toEqual([]); + expect(result.paths['/1-1/{id}/{path}']!.get!.tags).toEqual(['1', '2']); + expect(result.paths['/1-2/{id}/{path}']!.get!.tags).toEqual(['1']); + expect(result.paths['/2-1/{id}/{path}']!.get!.tags).toEqual([]); // versioned router paths expect(result.paths['/v1-1']!.get!.tags).toEqual(['v1']); expect(result.paths['/v1-2']!.get!.tags).toEqual(['v2', 'v3']); @@ -392,17 +392,17 @@ describe('generateOpenApiDocument', () => { ); // router paths - expect(result.paths['/1-1/{id}/{path*}']!.get).toMatchObject({ + expect(result.paths['/1-1/{id}/{path}']!.get).toMatchObject({ 'x-state': 'Technical Preview', }); - expect(result.paths['/1-2/{id}/{path*}']!.get).toMatchObject({ + expect(result.paths['/1-2/{id}/{path}']!.get).toMatchObject({ 'x-state': 'Beta', }); - expect(result.paths['/1-3/{id}/{path*}']!.get).not.toMatchObject({ + expect(result.paths['/1-3/{id}/{path}']!.get).not.toMatchObject({ 'x-state': expect.any(String), }); - expect(result.paths['/2-1/{id}/{path*}']!.get).not.toMatchObject({ + expect(result.paths['/2-1/{id}/{path}']!.get).not.toMatchObject({ 'x-state': expect.any(String), }); diff --git a/packages/kbn-router-to-openapispec/src/util.test.ts b/packages/kbn-router-to-openapispec/src/util.test.ts index f9692e57e1f50..e3011aa1a5a73 100644 --- a/packages/kbn-router-to-openapispec/src/util.test.ts +++ b/packages/kbn-router-to-openapispec/src/util.test.ts @@ -17,8 +17,9 @@ import { getPathParameters, createOpIdGenerator, GetOpId, + assignToPaths, + extractTags, } from './util'; -import { assignToPaths, extractTags } from './util'; describe('extractTags', () => { test.each([ @@ -115,9 +116,11 @@ describe('assignToPaths', () => { const paths = {}; assignToPaths(paths, '/foo', {}); assignToPaths(paths, '/bar/{id?}', {}); + assignToPaths(paths, '/bar/file/{path*}', {}); expect(paths).toEqual({ '/foo': {}, '/bar/{id}': {}, + '/bar/file/{path}': {}, }); }); }); @@ -320,7 +323,7 @@ describe('createOpIdGenerator', () => { { input: { method: 'get', - path: '/api/my/resource/{path*}', + path: '/api/my/resource/{path}', }, output: 'get-my-resource-path', }, diff --git a/packages/kbn-router-to-openapispec/src/util.ts b/packages/kbn-router-to-openapispec/src/util.ts index a5718fa92120f..1088259e73d05 100644 --- a/packages/kbn-router-to-openapispec/src/util.ts +++ b/packages/kbn-router-to-openapispec/src/util.ts @@ -132,7 +132,7 @@ export const assignToPaths = ( path: string, pathObject: OpenAPIV3.PathItemObject ): void => { - const pathName = path.replace('?', ''); + const pathName = path.replace(/[\?\*]/, ''); paths[pathName] = { ...paths[pathName], ...pathObject }; }; diff --git a/src/plugins/expression_error/public/expression_renderers/__stories__/error_renderer.stories.tsx b/src/plugins/expression_error/public/expression_renderers/__stories__/error_renderer.stories.tsx index 07451f1f2c325..b7f789f873de3 100644 --- a/src/plugins/expression_error/public/expression_renderers/__stories__/error_renderer.stories.tsx +++ b/src/plugins/expression_error/public/expression_renderers/__stories__/error_renderer.stories.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; +import { coreMock } from '@kbn/core/public/mocks'; import { Render } from '@kbn/presentation-util-plugin/public/__stories__'; import { getErrorRenderer } from '../error_renderer'; @@ -17,5 +18,8 @@ storiesOf('renderers/error', module).add('default', () => { const config = { error: thrownError, }; - return ; + + return ( + + ); }); diff --git a/src/plugins/expression_error/public/expression_renderers/debug_renderer.tsx b/src/plugins/expression_error/public/expression_renderers/debug_renderer.tsx index c3d4220038870..29fa69aa736b4 100644 --- a/src/plugins/expression_error/public/expression_renderers/debug_renderer.tsx +++ b/src/plugins/expression_error/public/expression_renderers/debug_renderer.tsx @@ -17,7 +17,6 @@ import { i18n } from '@kbn/i18n'; import { withSuspense } from '@kbn/presentation-util-plugin/public'; import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary'; -import { defaultTheme$ } from '@kbn/presentation-util-plugin/common'; import { JSON } from '../../common'; import { LazyDebugRenderComponent } from '../components'; @@ -38,8 +37,7 @@ const strings = { }; export const getDebugRenderer = - (theme$: Observable = defaultTheme$) => - (): ExpressionRenderDefinition => ({ + (theme$: Observable) => (): ExpressionRenderDefinition => ({ name: 'debug', displayName: strings.getDisplayName(), help: strings.getHelpDescription(), diff --git a/src/plugins/expression_error/public/expression_renderers/error_renderer.tsx b/src/plugins/expression_error/public/expression_renderers/error_renderer.tsx index c7865525b21f6..4ba3daa15d08c 100644 --- a/src/plugins/expression_error/public/expression_renderers/error_renderer.tsx +++ b/src/plugins/expression_error/public/expression_renderers/error_renderer.tsx @@ -21,7 +21,6 @@ import { import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary'; import { withSuspense } from '@kbn/presentation-util-plugin/public'; -import { defaultTheme$ } from '@kbn/presentation-util-plugin/common'; import { ErrorRendererConfig } from '../../common/types'; import { LazyErrorRenderComponent } from '../components'; @@ -39,8 +38,7 @@ const errorStrings = { const ErrorComponent = withSuspense(LazyErrorRenderComponent); export const getErrorRenderer = - (theme$: Observable = defaultTheme$) => - (): ExpressionRenderDefinition => ({ + (theme$: Observable) => (): ExpressionRenderDefinition => ({ name: 'error', displayName: errorStrings.getDisplayName(), help: errorStrings.getHelpDescription(), diff --git a/src/plugins/expression_image/public/expression_renderers/__stories__/image_renderer.stories.tsx b/src/plugins/expression_image/public/expression_renderers/__stories__/image_renderer.stories.tsx index 02679899b112d..07fb4db558bd5 100644 --- a/src/plugins/expression_image/public/expression_renderers/__stories__/image_renderer.stories.tsx +++ b/src/plugins/expression_image/public/expression_renderers/__stories__/image_renderer.stories.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; +import { coreMock } from '@kbn/core/public/mocks'; import { Render, waitFor } from '@kbn/presentation-util-plugin/public/__stories__'; import { getElasticLogo } from '@kbn/presentation-util-plugin/common'; import { getImageRenderer } from '../image_renderer'; @@ -20,7 +21,14 @@ const Renderer = ({ elasticLogo }: { elasticLogo: string }) => { mode: ImageMode.COVER, }; - return ; + return ( + + ); }; storiesOf('renderers/image', module).add( diff --git a/src/plugins/expression_image/public/expression_renderers/image_renderer.tsx b/src/plugins/expression_image/public/expression_renderers/image_renderer.tsx index 8954bd90dc974..e80281fc10f42 100644 --- a/src/plugins/expression_image/public/expression_renderers/image_renderer.tsx +++ b/src/plugins/expression_image/public/expression_renderers/image_renderer.tsx @@ -17,7 +17,7 @@ import { IInterpreterRenderHandlers, } from '@kbn/expressions-plugin/common'; import { i18n } from '@kbn/i18n'; -import { getElasticLogo, defaultTheme$, isValidUrl } from '@kbn/presentation-util-plugin/common'; +import { getElasticLogo, isValidUrl } from '@kbn/presentation-util-plugin/common'; import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary'; import { ImageRendererConfig } from '../../common/types'; @@ -34,8 +34,7 @@ const strings = { }; export const getImageRenderer = - (theme$: Observable = defaultTheme$) => - (): ExpressionRenderDefinition => ({ + (theme$: Observable) => (): ExpressionRenderDefinition => ({ name: 'image', displayName: strings.getDisplayName(), help: strings.getHelpDescription(), diff --git a/src/plugins/expression_metric/public/expression_renderers/__stories__/metric_renderer.stories.tsx b/src/plugins/expression_metric/public/expression_renderers/__stories__/metric_renderer.stories.tsx index 03c8f9d5048d5..5438bb4b4287a 100644 --- a/src/plugins/expression_metric/public/expression_renderers/__stories__/metric_renderer.stories.tsx +++ b/src/plugins/expression_metric/public/expression_renderers/__stories__/metric_renderer.stories.tsx @@ -9,6 +9,7 @@ import React, { CSSProperties } from 'react'; import { storiesOf } from '@storybook/react'; +import { coreMock } from '@kbn/core/public/mocks'; import { Style } from '@kbn/expressions-plugin/common'; import { Render } from '@kbn/presentation-util-plugin/public/__stories__'; import { getMetricRenderer } from '../metric_renderer'; @@ -37,6 +38,8 @@ const metricFontSpec: CSSProperties = { color: '#b83c6f', }; +const theme$ = coreMock.createStart().theme.theme$; + storiesOf('renderers/Metric', module) .add('with null metric', () => { const config: MetricRendererConfig = { @@ -46,7 +49,7 @@ storiesOf('renderers/Metric', module) label: '', metricFormat: '', }; - return ; + return ; }) .add('with number metric', () => { const config: MetricRendererConfig = { @@ -56,7 +59,7 @@ storiesOf('renderers/Metric', module) label: '', metricFormat: '', }; - return ; + return ; }) .add('with string metric', () => { const config: MetricRendererConfig = { @@ -66,7 +69,7 @@ storiesOf('renderers/Metric', module) label: '', metricFormat: '', }; - return ; + return ; }) .add('with label', () => { const config: MetricRendererConfig = { @@ -76,7 +79,7 @@ storiesOf('renderers/Metric', module) label: 'Average price', metricFormat: '', }; - return ; + return ; }) .add('with number metric and a specified format', () => { const config: MetricRendererConfig = { @@ -86,7 +89,7 @@ storiesOf('renderers/Metric', module) label: 'Average price', metricFormat: '0.00%', }; - return ; + return ; }) .add('with formatted string metric and a specified format', () => { const config: MetricRendererConfig = { @@ -96,7 +99,7 @@ storiesOf('renderers/Metric', module) label: 'Total Revenue', metricFormat: '$0a', }; - return ; + return ; }) .add('with invalid metricFont', () => { const config: MetricRendererConfig = { @@ -106,5 +109,5 @@ storiesOf('renderers/Metric', module) label: 'Total Revenue', metricFormat: '$0a', }; - return ; + return ; }); diff --git a/src/plugins/expression_metric/public/expression_renderers/metric_renderer.tsx b/src/plugins/expression_metric/public/expression_renderers/metric_renderer.tsx index b802f4f40dafb..db4b8575d5e26 100644 --- a/src/plugins/expression_metric/public/expression_renderers/metric_renderer.tsx +++ b/src/plugins/expression_metric/public/expression_renderers/metric_renderer.tsx @@ -19,7 +19,6 @@ import { import { i18n } from '@kbn/i18n'; import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary'; -import { defaultTheme$ } from '@kbn/presentation-util-plugin/common'; import { MetricRendererConfig } from '../../common/types'; const strings = { @@ -34,8 +33,7 @@ const strings = { }; export const getMetricRenderer = - (theme$: Observable = defaultTheme$) => - (): ExpressionRenderDefinition => ({ + (theme$: Observable) => (): ExpressionRenderDefinition => ({ name: 'metric', displayName: strings.getDisplayName(), help: strings.getHelpDescription(), diff --git a/src/plugins/expression_repeat_image/public/expression_renderers/__stories__/repeat_image_renderer.stories.tsx b/src/plugins/expression_repeat_image/public/expression_renderers/__stories__/repeat_image_renderer.stories.tsx index fa666d98ebb9a..a5f58b97970cf 100644 --- a/src/plugins/expression_repeat_image/public/expression_renderers/__stories__/repeat_image_renderer.stories.tsx +++ b/src/plugins/expression_repeat_image/public/expression_renderers/__stories__/repeat_image_renderer.stories.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; +import { coreMock } from '@kbn/core/public/mocks'; import { Render } from '@kbn/presentation-util-plugin/public/__stories__'; import { getElasticLogo, getElasticOutline } from '@kbn/presentation-util-plugin/common'; import { waitFor } from '@kbn/presentation-util-plugin/public/__stories__'; @@ -29,7 +30,13 @@ const Renderer = ({ emptyImage: elasticOutline, }; - return ; + return ( + + ); }; storiesOf('enderers/repeatImage', module).add( diff --git a/src/plugins/expression_repeat_image/public/expression_renderers/repeat_image_renderer.tsx b/src/plugins/expression_repeat_image/public/expression_renderers/repeat_image_renderer.tsx index 6dcc132937060..9a35459880889 100644 --- a/src/plugins/expression_repeat_image/public/expression_renderers/repeat_image_renderer.tsx +++ b/src/plugins/expression_repeat_image/public/expression_renderers/repeat_image_renderer.tsx @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n-react'; import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary'; -import { defaultTheme$, getElasticOutline, isValidUrl } from '@kbn/presentation-util-plugin/common'; +import { getElasticOutline, isValidUrl } from '@kbn/presentation-util-plugin/common'; import { RepeatImageRendererConfig } from '../../common/types'; const strings = { @@ -35,8 +35,7 @@ const strings = { }; export const getRepeatImageRenderer = - (theme$: Observable = defaultTheme$) => - (): ExpressionRenderDefinition => ({ + (theme$: Observable) => (): ExpressionRenderDefinition => ({ name: 'repeatImage', displayName: strings.getDisplayName(), help: strings.getHelpDescription(), diff --git a/src/plugins/expression_reveal_image/public/expression_renderers/__stories__/reveal_image_renderer.stories.tsx b/src/plugins/expression_reveal_image/public/expression_renderers/__stories__/reveal_image_renderer.stories.tsx index 0c1a2cb5ca169..664cc97117791 100644 --- a/src/plugins/expression_reveal_image/public/expression_renderers/__stories__/reveal_image_renderer.stories.tsx +++ b/src/plugins/expression_reveal_image/public/expression_renderers/__stories__/reveal_image_renderer.stories.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; +import { coreMock } from '@kbn/core/public/mocks'; import { getElasticOutline, getElasticLogo } from '@kbn/presentation-util-plugin/common'; import { Render, waitFor } from '@kbn/presentation-util-plugin/public/__stories__'; import { getRevealImageRenderer } from '..'; @@ -27,7 +28,13 @@ const Renderer = ({ origin: Origin.LEFT, percent: 0.45, }; - return ; + + return ( + + ); }; storiesOf('renderers/revealImage', module).add( diff --git a/src/plugins/expression_reveal_image/public/expression_renderers/reveal_image_renderer.tsx b/src/plugins/expression_reveal_image/public/expression_renderers/reveal_image_renderer.tsx index 5beb58ef60f39..fbfe479225ece 100644 --- a/src/plugins/expression_reveal_image/public/expression_renderers/reveal_image_renderer.tsx +++ b/src/plugins/expression_reveal_image/public/expression_renderers/reveal_image_renderer.tsx @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n-react'; import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary'; -import { defaultTheme$ } from '@kbn/presentation-util-plugin/common'; import { RevealImageRendererConfig } from '../../common/types'; export const strings = { @@ -35,8 +34,7 @@ export const strings = { }; export const getRevealImageRenderer = - (theme$: Observable = defaultTheme$) => - (): ExpressionRenderDefinition => ({ + (theme$: Observable) => (): ExpressionRenderDefinition => ({ name: 'revealImage', displayName: strings.getDisplayName(), help: strings.getHelpDescription(), diff --git a/src/plugins/expression_shape/public/expression_renderers/__stories__/progress_renderer.stories.tsx b/src/plugins/expression_shape/public/expression_renderers/__stories__/progress_renderer.stories.tsx index 22e8864d1b7de..0f93314ee0816 100644 --- a/src/plugins/expression_shape/public/expression_renderers/__stories__/progress_renderer.stories.tsx +++ b/src/plugins/expression_shape/public/expression_renderers/__stories__/progress_renderer.stories.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { Render } from '@kbn/presentation-util-plugin/public/__stories__'; +import { coreMock } from '@kbn/core/public/mocks'; import { getProgressRenderer } from '../progress_renderer'; import { Progress } from '../../../common'; @@ -30,5 +31,7 @@ storiesOf('renderers/progress', module).add('default', () => { valueWeight: 15, }; - return ; + return ( + + ); }); diff --git a/src/plugins/expression_shape/public/expression_renderers/__stories__/shape_renderer.stories.tsx b/src/plugins/expression_shape/public/expression_renderers/__stories__/shape_renderer.stories.tsx index 77b54ae87239b..d089174c60325 100644 --- a/src/plugins/expression_shape/public/expression_renderers/__stories__/shape_renderer.stories.tsx +++ b/src/plugins/expression_shape/public/expression_renderers/__stories__/shape_renderer.stories.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; +import { coreMock } from '@kbn/core/public/mocks'; import { Render } from '@kbn/presentation-util-plugin/public/__stories__'; import { getShapeRenderer } from '..'; import { Shape } from '../../../common/types'; @@ -23,5 +24,7 @@ storiesOf('renderers/shape', module).add('default', () => { maintainAspect: true, }; - return ; + return ( + + ); }); diff --git a/src/plugins/expression_shape/public/expression_renderers/progress_renderer.tsx b/src/plugins/expression_shape/public/expression_renderers/progress_renderer.tsx index 80904cf12f2b6..ed7629a7d87a0 100644 --- a/src/plugins/expression_shape/public/expression_renderers/progress_renderer.tsx +++ b/src/plugins/expression_shape/public/expression_renderers/progress_renderer.tsx @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n-react'; import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary'; -import { defaultTheme$ } from '@kbn/presentation-util-plugin/common'; import { ProgressRendererConfig } from '../../common/types'; const strings = { @@ -35,8 +34,7 @@ const strings = { }; export const getProgressRenderer = - (theme$: Observable = defaultTheme$) => - (): ExpressionRenderDefinition => ({ + (theme$: Observable) => (): ExpressionRenderDefinition => ({ name: 'progress', displayName: strings.getDisplayName(), help: strings.getHelpDescription(), diff --git a/src/plugins/expression_shape/public/expression_renderers/shape_renderer.tsx b/src/plugins/expression_shape/public/expression_renderers/shape_renderer.tsx index 06c442ad2d8cd..650033aa4542d 100644 --- a/src/plugins/expression_shape/public/expression_renderers/shape_renderer.tsx +++ b/src/plugins/expression_shape/public/expression_renderers/shape_renderer.tsx @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n-react'; import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary'; -import { defaultTheme$ } from '@kbn/presentation-util-plugin/common'; import { ShapeRendererConfig } from '../../common/types'; const strings = { @@ -35,8 +34,7 @@ const strings = { }; export const getShapeRenderer = - (theme$: Observable = defaultTheme$) => - (): ExpressionRenderDefinition => ({ + (theme$: Observable) => (): ExpressionRenderDefinition => ({ name: 'shape', displayName: strings.getDisplayName(), help: strings.getHelpDescription(), diff --git a/src/plugins/presentation_util/common/index.ts b/src/plugins/presentation_util/common/index.ts index 6917c52f4edc9..b0350ca4a20f4 100644 --- a/src/plugins/presentation_util/common/index.ts +++ b/src/plugins/presentation_util/common/index.ts @@ -36,7 +36,6 @@ export { } from './labs'; export { - defaultTheme$, getElasticLogo, getElasticOutline, isValidUrl, diff --git a/src/plugins/presentation_util/common/lib/utils/default_theme.ts b/src/plugins/presentation_util/common/lib/utils/default_theme.ts deleted file mode 100644 index 9c86456bc2513..0000000000000 --- a/src/plugins/presentation_util/common/lib/utils/default_theme.ts +++ /dev/null @@ -1,15 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import type { CoreTheme } from '@kbn/core/public'; -import { Observable } from 'rxjs'; - -export const defaultTheme$: Observable = new Observable((subscriber) => - subscriber.next({ darkMode: false }) -); diff --git a/src/plugins/presentation_util/common/lib/utils/index.ts b/src/plugins/presentation_util/common/lib/utils/index.ts index 20b1d8450e5a5..091aa216aa835 100644 --- a/src/plugins/presentation_util/common/lib/utils/index.ts +++ b/src/plugins/presentation_util/common/lib/utils/index.ts @@ -11,7 +11,6 @@ export * from './dataurl'; export * from './httpurl'; export * from './resolve_dataurl'; export * from './url'; -export { defaultTheme$ } from './default_theme'; export async function getElasticLogo() { return await import('./elastic_logo'); diff --git a/src/plugins/unified_search/public/dataview_picker/change_dataview.styles.ts b/src/plugins/unified_search/public/dataview_picker/change_dataview.styles.ts index 9727c8f6e1593..21ece7f82e88d 100644 --- a/src/plugins/unified_search/public/dataview_picker/change_dataview.styles.ts +++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.styles.ts @@ -12,15 +12,18 @@ import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count' import { DataViewListItemEnhanced } from './dataview_list'; const MIN_WIDTH = 300; +const MAX_MOBILE_WIDTH = 350; export const changeDataViewStyles = ({ fullWidth, dataViewsList, theme, + isMobile, }: { fullWidth?: boolean; dataViewsList: DataViewListItemEnhanced[]; theme: EuiThemeComputed; + isMobile: boolean; }) => { return { trigger: { @@ -30,7 +33,10 @@ export const changeDataViewStyles = ({ borderBottomLeftRadius: 0, }, popoverContent: { - width: calculateWidthFromEntries(dataViewsList, ['name', 'id'], { minWidth: MIN_WIDTH }), + width: calculateWidthFromEntries(dataViewsList, ['name', 'id'], { + minWidth: MIN_WIDTH, + ...(isMobile && { maxWidth: MAX_MOBILE_WIDTH }), + }), }, }; }; diff --git a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx index d89621d598679..06ed558ce7a01 100644 --- a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx +++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx @@ -17,6 +17,7 @@ import { EuiContextMenuItem, useEuiTheme, useGeneratedHtmlId, + useIsWithinBreakpoints, EuiIcon, EuiText, EuiContextMenuPanelProps, @@ -68,10 +69,13 @@ export function ChangeDataView({ const kibana = useKibana(); const { application, data, dataViews, dataViewEditor } = kibana.services; + const isMobile = useIsWithinBreakpoints(['xs']); + const styles = changeDataViewStyles({ fullWidth: trigger.fullWidth, dataViewsList, theme: euiTheme, + isMobile, }); // Create a reusable id to ensure search input is the first focused item in the popover even though it's not the first item diff --git a/test/functional/screenshots/.empty b/test/functional/screenshots/.empty deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/simulated_function_calling_callout.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/simulated_function_calling_callout.tsx index 26eb589b25dfc..b570a0cae11f0 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/simulated_function_calling_callout.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/simulated_function_calling_callout.tsx @@ -19,7 +19,7 @@ export function SimulatedFunctionCallingCallout() { {i18n.translate('xpack.aiAssistant.simulatedFunctionCallingCalloutLabel', { defaultMessage: - 'Simulated function calling is enabled. You might see degradated performance.', + 'Simulated function calling is enabled. You might see degraded performance.', })} diff --git a/x-pack/packages/kbn-cloud-security-posture/common/utils/helpers.test.ts b/x-pack/packages/kbn-cloud-security-posture/common/utils/helpers.test.ts index 0248cdf9b6e36..04cb76f4441c5 100644 --- a/x-pack/packages/kbn-cloud-security-posture/common/utils/helpers.test.ts +++ b/x-pack/packages/kbn-cloud-security-posture/common/utils/helpers.test.ts @@ -10,6 +10,7 @@ import { defaultErrorMessage, buildMutedRulesFilter, buildEntityFlyoutPreviewQuery, + buildEntityAlertsQuery, } from './helpers'; const fallbackMessage = 'thisIsAFallBackMessage'; @@ -182,4 +183,78 @@ describe('test helper methods', () => { expect(buildEntityFlyoutPreviewQuery(field)).toEqual(expectedQuery); }); }); + + describe('buildEntityAlertsQuery', () => { + const getExpectedAlertsQuery = (size?: number) => { + return { + size: size || 0, + _source: false, + fields: [ + '_id', + '_index', + 'kibana.alert.rule.uuid', + 'kibana.alert.severity', + 'kibana.alert.rule.name', + 'kibana.alert.workflow_status', + ], + query: { + bool: { + filter: [ + { + bool: { + must: [], + filter: [ + { + match_phrase: { + 'host.name': { + query: 'exampleHost', + }, + }, + }, + ], + should: [], + must_not: [], + }, + }, + { + range: { + '@timestamp': { + gte: 'Today', + lte: 'Tomorrow', + }, + }, + }, + { + terms: { + 'kibana.alert.workflow_status': ['open', 'acknowledged'], + }, + }, + ], + }, + }, + }; + }; + + it('should return the correct query when given all params', () => { + const field = 'host.name'; + const query = 'exampleHost'; + const to = 'Tomorrow'; + const from = 'Today'; + const size = 100; + + expect(buildEntityAlertsQuery(field, to, from, query, size)).toEqual( + getExpectedAlertsQuery(size) + ); + }); + + it('should return the correct query when not given size', () => { + const field = 'host.name'; + const query = 'exampleHost'; + const to = 'Tomorrow'; + const from = 'Today'; + const size = undefined; + + expect(buildEntityAlertsQuery(field, to, from, query)).toEqual(getExpectedAlertsQuery(size)); + }); + }); }); diff --git a/x-pack/packages/kbn-cloud-security-posture/common/utils/helpers.ts b/x-pack/packages/kbn-cloud-security-posture/common/utils/helpers.ts index 7039c99af6d53..bd531fa63804f 100644 --- a/x-pack/packages/kbn-cloud-security-posture/common/utils/helpers.ts +++ b/x-pack/packages/kbn-cloud-security-posture/common/utils/helpers.ts @@ -5,6 +5,7 @@ * 2.0. */ import { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types'; + import { i18n } from '@kbn/i18n'; import type { CspBenchmarkRulesStates } from '../schema/rules/latest'; @@ -62,3 +63,59 @@ export const buildEntityFlyoutPreviewQuery = (field: string, queryValue?: string }, }; }; + +export const buildEntityAlertsQuery = ( + field: string, + to: string, + from: string, + queryValue?: string, + size?: number +) => { + return { + size: size || 0, + _source: false, + fields: [ + '_id', + '_index', + 'kibana.alert.rule.uuid', + 'kibana.alert.severity', + 'kibana.alert.rule.name', + 'kibana.alert.workflow_status', + ], + query: { + bool: { + filter: [ + { + bool: { + must: [], + filter: [ + { + match_phrase: { + [field]: { + query: queryValue, + }, + }, + }, + ], + should: [], + must_not: [], + }, + }, + { + range: { + '@timestamp': { + gte: from, + lte: to, + }, + }, + }, + { + terms: { + 'kibana.alert.workflow_status': ['open', 'acknowledged'], + }, + }, + ], + }, + }, + }; +}; diff --git a/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_misconfiguration_findings.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_misconfiguration_findings.ts index 40880b132537d..9bbaedf587dde 100644 --- a/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_misconfiguration_findings.ts +++ b/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_misconfiguration_findings.ts @@ -40,10 +40,11 @@ export const useMisconfigurationFindings = (options: UseCspOptions) => { params: buildMisconfigurationsFindingsQuery(options, rulesStates!), }) ); - if (!aggregations) throw new Error('expected aggregations to be defined'); + if (!aggregations && options.ignore_unavailable === false) + throw new Error('expected aggregations to be defined'); return { - count: getMisconfigurationAggregationCount(aggregations.count.buckets), + count: getMisconfigurationAggregationCount(aggregations?.count.buckets), rows: hits.hits.map((finding) => ({ result: finding._source?.result, rule: finding?._source?.rule, diff --git a/x-pack/plugins/alerting/common/routes/rule/common/flapping/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/rule/common/flapping/schemas/v1.ts index 4844a9e808ef5..acdd35854df3f 100644 --- a/x-pack/plugins/alerting/common/routes/rule/common/flapping/schemas/v1.ts +++ b/x-pack/plugins/alerting/common/routes/rule/common/flapping/schemas/v1.ts @@ -17,13 +17,24 @@ import { validateFlapping as validateFlappingV1 } from '../../../validation/vali export const flappingSchema = schema.object( { look_back_window: schema.number({ + meta: { description: 'The minimum number of runs in which the threshold must be met.' }, min: MIN_LOOK_BACK_WINDOW_V1, max: MAX_LOOK_BACK_WINDOW_V1, }), status_change_threshold: schema.number({ + meta: { + description: + 'The minimum number of times an alert must switch states in the look back window.', + }, min: MIN_STATUS_CHANGE_THRESHOLD_V1, max: MAX_STATUS_CHANGE_THRESHOLD_V1, }), }, - { validate: validateFlappingV1 } + { + validate: validateFlappingV1, + meta: { + description: + 'When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.', + }, + } ); diff --git a/x-pack/plugins/cases/common/constants/index.ts b/x-pack/plugins/cases/common/constants/index.ts index 557899e322ae7..aa3855807cea2 100644 --- a/x-pack/plugins/cases/common/constants/index.ts +++ b/x-pack/plugins/cases/common/constants/index.ts @@ -62,6 +62,8 @@ export const CASE_FIND_USER_ACTIONS_URL = `${CASE_USER_ACTIONS_URL}/_find` as co export const CASE_ALERTS_URL = `${CASES_URL}/alerts/{alert_id}` as const; export const CASE_DETAILS_ALERTS_URL = `${CASE_DETAILS_URL}/alerts` as const; +export const CASE_FILES_URL = `${CASE_DETAILS_URL}/files` as const; + /** * Internal routes */ @@ -139,6 +141,7 @@ export const MAX_TEMPLATE_DESCRIPTION_LENGTH = 1000 as const; export const MAX_TEMPLATES_LENGTH = 10 as const; export const MAX_TEMPLATE_TAG_LENGTH = 50 as const; export const MAX_TAGS_PER_TEMPLATE = 10 as const; +export const MAX_FILENAME_LENGTH = 160 as const; /** * Cases features diff --git a/x-pack/plugins/cases/common/schema/index.test.ts b/x-pack/plugins/cases/common/schema/index.test.ts index 64eb2ad393fcb..8a20943831cd0 100644 --- a/x-pack/plugins/cases/common/schema/index.test.ts +++ b/x-pack/plugins/cases/common/schema/index.test.ts @@ -11,6 +11,7 @@ import { limitedArraySchema, limitedNumberSchema, limitedStringSchema, + mimeTypeString, NonEmptyString, paginationSchema, limitedNumberAsIntegerSchema, @@ -321,14 +322,32 @@ describe('schema', () => { }); }); + describe('mimeTypeString', () => { + it('works correctly when the value is an allowed mime type', () => { + expect(PathReporter.report(mimeTypeString.decode('image/jpx'))).toMatchInlineSnapshot(` + Array [ + "No errors!", + ] + `); + }); + + it('fails when the value is not an allowed mime type', () => { + expect(PathReporter.report(mimeTypeString.decode('foo/bar'))).toMatchInlineSnapshot(` + Array [ + "The mime type field value foo/bar is not allowed.", + ] + `); + }); + }); + describe('limitedNumberAsIntegerSchema', () => { it('works correctly the number is safe integer', () => { expect(PathReporter.report(limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode(1))) .toMatchInlineSnapshot(` - Array [ - "No errors!", - ] - `); + Array [ + "No errors!", + ] + `); }); it('fails when given a number that is lower than the minimum', () => { diff --git a/x-pack/plugins/cases/common/schema/index.ts b/x-pack/plugins/cases/common/schema/index.ts index 0bcbdcfb2c480..1ddd4a161b6e6 100644 --- a/x-pack/plugins/cases/common/schema/index.ts +++ b/x-pack/plugins/cases/common/schema/index.ts @@ -11,6 +11,7 @@ import { either } from 'fp-ts/lib/Either'; import { MAX_DOCS_PER_PAGE } from '../constants'; import type { PartialPaginationType } from './types'; import { PaginationSchemaRt } from './types'; +import { ALLOWED_MIME_TYPES } from '../constants/mime_types'; export interface LimitedSchemaType { fieldName: string; @@ -194,3 +195,17 @@ export const regexStringRt = ({ codec, pattern, message }: RegexStringSchemaType }), rt.identity ); + +export const mimeTypeString = new rt.Type( + 'mimeTypeString', + rt.string.is, + (input, context) => + either.chain(rt.string.validate(input, context), (s) => { + if (!ALLOWED_MIME_TYPES.includes(s)) { + return rt.failure(input, context, `The mime type field value ${s} is not allowed.`); + } + + return rt.success(s); + }), + rt.identity +); diff --git a/x-pack/plugins/cases/common/types/api/attachment/v1.test.ts b/x-pack/plugins/cases/common/types/api/attachment/v1.test.ts index e08cff94717e4..28c0b5d8da789 100644 --- a/x-pack/plugins/cases/common/types/api/attachment/v1.test.ts +++ b/x-pack/plugins/cases/common/types/api/attachment/v1.test.ts @@ -6,7 +6,11 @@ */ import { PathReporter } from 'io-ts/lib/PathReporter'; -import { MAX_BULK_CREATE_ATTACHMENTS, MAX_COMMENT_LENGTH } from '../../../constants'; +import { + MAX_BULK_CREATE_ATTACHMENTS, + MAX_COMMENT_LENGTH, + MAX_FILENAME_LENGTH, +} from '../../../constants'; import { AttachmentType } from '../../domain/attachment/v1'; import { AttachmentPatchRequestRt, @@ -17,6 +21,7 @@ import { BulkGetAttachmentsRequestRt, BulkGetAttachmentsResponseRt, FindAttachmentsQueryParamsRt, + PostFileAttachmentRequestRt, } from './v1'; describe('Attachments', () => { @@ -389,4 +394,49 @@ describe('Attachments', () => { }); }); }); + + describe('PostFileAttachmentRequestRt', () => { + const defaultRequest = { + file: 'Solve this fast!', + filename: 'filename', + }; + + it('has the expected attributes in request', () => { + const query = PostFileAttachmentRequestRt.decode(defaultRequest); + + expect(query).toStrictEqual({ + _tag: 'Right', + right: defaultRequest, + }); + }); + + it('removes foo:bar attributes from request', () => { + const query = PostFileAttachmentRequestRt.decode({ ...defaultRequest, foo: 'bar' }); + + expect(query).toStrictEqual({ + _tag: 'Right', + right: defaultRequest, + }); + }); + + describe('errors', () => { + it('throws an error when the filename is too long', () => { + const longFilename = 'x'.repeat(MAX_FILENAME_LENGTH + 1); + + expect( + PathReporter.report( + PostFileAttachmentRequestRt.decode({ ...defaultRequest, filename: longFilename }) + ) + ).toContain('The length of the filename is too long. The maximum length is 160.'); + }); + + it('throws an error when the filename is too small', () => { + expect( + PathReporter.report( + PostFileAttachmentRequestRt.decode({ ...defaultRequest, filename: '' }) + ) + ).toContain('The filename field cannot be an empty string.'); + }); + }); + }); }); diff --git a/x-pack/plugins/cases/common/types/api/attachment/v1.ts b/x-pack/plugins/cases/common/types/api/attachment/v1.ts index 3d9cb02a5ce55..a6a2c3f97b39e 100644 --- a/x-pack/plugins/cases/common/types/api/attachment/v1.ts +++ b/x-pack/plugins/cases/common/types/api/attachment/v1.ts @@ -12,6 +12,7 @@ import { MAX_COMMENTS_PER_PAGE, MAX_COMMENT_LENGTH, MAX_DELETE_FILES, + MAX_FILENAME_LENGTH, } from '../../../constants'; import { limitedArraySchema, @@ -47,7 +48,23 @@ export const BulkDeleteFileAttachmentsRequestRt = rt.strict({ }), }); +export const PostFileAttachmentRequestRt = rt.intersection([ + rt.strict({ + file: rt.unknown, + }), + rt.exact( + rt.partial({ + filename: limitedStringSchema({ fieldName: 'filename', min: 1, max: MAX_FILENAME_LENGTH }), + }) + ), +]); + export type BulkDeleteFileAttachmentsRequest = rt.TypeOf; +export type PostFileAttachmentRequest = rt.TypeOf; + +/** + * Attachments + */ const BasicAttachmentRequestRt = rt.union([ UserCommentAttachmentPayloadRt, diff --git a/x-pack/plugins/cases/common/types/domain/attachment/v1.ts b/x-pack/plugins/cases/common/types/domain/attachment/v1.ts index df6193bd3ae36..1dbf1129f58c5 100644 --- a/x-pack/plugins/cases/common/types/domain/attachment/v1.ts +++ b/x-pack/plugins/cases/common/types/domain/attachment/v1.ts @@ -6,8 +6,10 @@ */ import * as rt from 'io-ts'; +import { limitedStringSchema, mimeTypeString } from '../../../schema'; import { jsonValueRt } from '../../../api'; import { UserRt } from '../user/v1'; +import { MAX_FILENAME_LENGTH } from '../../../constants'; /** * Files @@ -35,6 +37,11 @@ export const AttachmentAttributesBasicRt = rt.strict({ updated_by: rt.union([UserRt, rt.null]), }); +export const FileAttachmentMetadataPayloadRt = rt.strict({ + mimeType: mimeTypeString, + filename: limitedStringSchema({ fieldName: 'filename', min: 1, max: MAX_FILENAME_LENGTH }), +}); + /** * User comment */ diff --git a/x-pack/plugins/cases/docs/openapi/bundled.json b/x-pack/plugins/cases/docs/openapi/bundled.json index 6b84ab8e090fa..aaf7b7afaec24 100644 --- a/x-pack/plugins/cases/docs/openapi/bundled.json +++ b/x-pack/plugins/cases/docs/openapi/bundled.json @@ -1869,6 +1869,61 @@ } } } + }, + "/api/cases/{caseId}/files": { + "post": { + "summary": "Attach a file to a case", + "operationId": "addCaseFileDefaultSpace", + "description": "Attach a file to a case. You must have `all` privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're updating. The request must include:\n- The `Content-Type: multipart/form-data` HTTP header.\n- The location of the file that is being uploaded.\n", + "tags": [ + "cases" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/case_id" + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/add_case_file_request" + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/case_response_properties" + }, + "examples": { + "addCaseFileResponse": { + "$ref": "#/components/examples/add_comment_response" + } + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/4xx_response" + } + } + } + } + } + } } }, "components": { @@ -4798,6 +4853,25 @@ "example": "create_case" } } + }, + "add_case_file_request": { + "title": "Add case file request properties", + "required": [ + "file" + ], + "description": "Defines the file that will be attached to the case. Optional parameters will be generated automatically from the file metadata if not defined.", + "type": "object", + "properties": { + "file": { + "description": "The file being attached to the case.", + "type": "string", + "format": "binary" + }, + "filename": { + "description": "The desired name of the file being attached to the case, it can be different than the name of the file in the filesystem. **This should not include the file extension.**", + "type": "string" + } + } } }, "examples": { diff --git a/x-pack/plugins/cases/docs/openapi/bundled.yaml b/x-pack/plugins/cases/docs/openapi/bundled.yaml index 40c86d41e1b26..5cfcca36f309a 100644 --- a/x-pack/plugins/cases/docs/openapi/bundled.yaml +++ b/x-pack/plugins/cases/docs/openapi/bundled.yaml @@ -1208,6 +1208,41 @@ paths: application/json: schema: $ref: '#/components/schemas/4xx_response' + /api/cases/{caseId}/files: + post: + summary: Attach a file to a case + operationId: addCaseFileDefaultSpace + description: | + Attach a file to a case. You must have `all` privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're updating. The request must include: + - The `Content-Type: multipart/form-data` HTTP header. + - The location of the file that is being uploaded. + tags: + - cases + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/case_id' + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/add_case_file_request' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '#/components/schemas/case_response_properties' + examples: + addCaseFileResponse: + $ref: '#/components/examples/add_comment_response' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/4xx_response' components: parameters: kbn_xsrf: @@ -3331,6 +3366,20 @@ components: - settings - severity example: create_case + add_case_file_request: + title: Add case file request properties + required: + - file + description: Defines the file that will be attached to the case. Optional parameters will be generated automatically from the file metadata if not defined. + type: object + properties: + file: + description: The file being attached to the case. + type: string + format: binary + filename: + description: The desired name of the file being attached to the case, it can be different than the name of the file in the filesystem. **This should not include the file extension.** + type: string examples: create_case_request: summary: Create a security case that uses a Jira connector. diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/add_case_file_request.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/add_case_file_request.yaml new file mode 100644 index 0000000000000..d114a7ba1be47 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/add_case_file_request.yaml @@ -0,0 +1,13 @@ +title: Add case file request properties +required: + - file +description: Defines the file that will be attached to the case. Optional parameters will be generated automatically from the file metadata if not defined. +type: object +properties: + file: + description: The file being attached to the case. + type: string + format: binary + filename: + description: The desired name of the file being attached to the case, it can be different than the name of the file in the filesystem. **This should not include the file extension.** + type: string diff --git a/x-pack/plugins/cases/docs/openapi/entrypoint.yaml b/x-pack/plugins/cases/docs/openapi/entrypoint.yaml index 764de3e6570fb..09280d6f33ff9 100644 --- a/x-pack/plugins/cases/docs/openapi/entrypoint.yaml +++ b/x-pack/plugins/cases/docs/openapi/entrypoint.yaml @@ -14,7 +14,7 @@ tags: servers: - url: / paths: -# Paths in the default space + # Paths in the default space '/api/cases': $ref: 'paths/api@cases.yaml' '/api/cases/_find': @@ -49,41 +49,43 @@ paths: $ref: 'paths/api@cases@{caseid}@user_actions@_find.yaml' '/api/cases/configure/connectors/_find': $ref: paths/api@cases@configure@connectors@_find.yaml + '/api/cases/{caseId}/files': + $ref: 'paths/api@cases@{caseid}@files.yaml' # Paths with space identifiers - # '/s/{spaceId}/api/cases': - # $ref: 'paths/s@{spaceid}@api@cases.yaml' - # '/s/{spaceId}/api/cases/_find': - # $ref: 'paths/s@{spaceid}@api@cases@_find.yaml' - # '/s/{spaceId}/api/cases/alerts/{alertId}': - # $ref: 'paths/s@{spaceid}@api@cases@alerts@{alertid}.yaml' - # '/s/{spaceId}/api/cases/configure': - # $ref: paths/s@{spaceid}@api@cases@configure.yaml - # '/s/{spaceId}/api/cases/configure/{configurationId}': - # $ref: paths/s@{spaceid}@api@cases@configure@{configurationid}.yaml - # '/s/{spaceId}/api/cases/configure/connectors/_find': - # $ref: paths/s@{spaceid}@api@cases@configure@connectors@_find.yaml - # '/s/{spaceId}/api/cases/reporters': - # $ref: 'paths/s@{spaceid}@api@cases@reporters.yaml' - # '/s/{spaceId}/api/cases/status': - # $ref: 'paths/s@{spaceid}@api@cases@status.yaml' - # '/s/{spaceId}/api/cases/tags': - # $ref: 'paths/s@{spaceid}@api@cases@tags.yaml' - # '/s/{spaceId}/api/cases/{caseId}': - # $ref: 'paths/s@{spaceid}@api@cases@{caseid}.yaml' - # '/s/{spaceId}/api/cases/{caseId}/alerts': - # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@alerts.yaml' - # '/s/{spaceId}/api/cases/{caseId}/comments': - # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments.yaml' - # '/s/{spaceId}/api/cases/{caseId}/comments/_find': - # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments@_find.yaml' - # '/s/{spaceId}/api/cases/{caseId}/comments/{commentId}': - # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments@{commentid}.yaml' - # '/s/{spaceId}/api/cases/{caseId}/connector/{connectorId}/_push': - # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@connector@{connectorid}@_push.yaml' - # '/s/{spaceId}/api/cases/{caseId}/user_actions': - # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@user_actions.yaml' - # '/s/{spaceId}/api/cases/{caseId}/user_actions/_find': - # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@user_actions@_find.yaml' +# '/s/{spaceId}/api/cases': +# $ref: 'paths/s@{spaceid}@api@cases.yaml' +# '/s/{spaceId}/api/cases/_find': +# $ref: 'paths/s@{spaceid}@api@cases@_find.yaml' +# '/s/{spaceId}/api/cases/alerts/{alertId}': +# $ref: 'paths/s@{spaceid}@api@cases@alerts@{alertid}.yaml' +# '/s/{spaceId}/api/cases/configure': +# $ref: paths/s@{spaceid}@api@cases@configure.yaml +# '/s/{spaceId}/api/cases/configure/{configurationId}': +# $ref: paths/s@{spaceid}@api@cases@configure@{configurationid}.yaml +# '/s/{spaceId}/api/cases/configure/connectors/_find': +# $ref: paths/s@{spaceid}@api@cases@configure@connectors@_find.yaml +# '/s/{spaceId}/api/cases/reporters': +# $ref: 'paths/s@{spaceid}@api@cases@reporters.yaml' +# '/s/{spaceId}/api/cases/status': +# $ref: 'paths/s@{spaceid}@api@cases@status.yaml' +# '/s/{spaceId}/api/cases/tags': +# $ref: 'paths/s@{spaceid}@api@cases@tags.yaml' +# '/s/{spaceId}/api/cases/{caseId}': +# $ref: 'paths/s@{spaceid}@api@cases@{caseid}.yaml' +# '/s/{spaceId}/api/cases/{caseId}/alerts': +# $ref: 'paths/s@{spaceid}@api@cases@{caseid}@alerts.yaml' +# '/s/{spaceId}/api/cases/{caseId}/comments': +# $ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments.yaml' +# '/s/{spaceId}/api/cases/{caseId}/comments/_find': +# $ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments@_find.yaml' +# '/s/{spaceId}/api/cases/{caseId}/comments/{commentId}': +# $ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments@{commentid}.yaml' +# '/s/{spaceId}/api/cases/{caseId}/connector/{connectorId}/_push': +# $ref: 'paths/s@{spaceid}@api@cases@{caseid}@connector@{connectorid}@_push.yaml' +# '/s/{spaceId}/api/cases/{caseId}/user_actions': +# $ref: 'paths/s@{spaceid}@api@cases@{caseid}@user_actions.yaml' +# '/s/{spaceId}/api/cases/{caseId}/user_actions/_find': +# $ref: 'paths/s@{spaceid}@api@cases@{caseid}@user_actions@_find.yaml' # components: # securitySchemes: # basicAuth: diff --git a/x-pack/plugins/cases/docs/openapi/paths/api@cases@{caseid}@files.yaml b/x-pack/plugins/cases/docs/openapi/paths/api@cases@{caseid}@files.yaml new file mode 100644 index 0000000000000..751e61cd75402 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/paths/api@cases@{caseid}@files.yaml @@ -0,0 +1,40 @@ +post: + summary: Attach a file to a case + operationId: addCaseFileDefaultSpace + description: > + Attach a file to a case. + You must have `all` privileges for the **Cases** feature in the + **Management**, **Observability**, or **Security** section of the Kibana + feature privileges, depending on the owner of the case you're updating. + The request must include: + + - The `Content-Type: multipart/form-data` HTTP header. + + - The location of the file that is being uploaded. + tags: + - cases + parameters: + - $ref: '../components/headers/kbn_xsrf.yaml' + - $ref: '../components/parameters/case_id.yaml' + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: '../components/schemas/add_case_file_request.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '../components/schemas/case_response_properties.yaml' + examples: + addCaseFileResponse: + $ref: '../components/examples/add_comment_response.yaml' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/4xx_response.yaml' diff --git a/x-pack/plugins/cases/server/client/attachments/add_file.test.ts b/x-pack/plugins/cases/server/client/attachments/add_file.test.ts new file mode 100644 index 0000000000000..16f803389f54a --- /dev/null +++ b/x-pack/plugins/cases/server/client/attachments/add_file.test.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Readable } from 'stream'; +import type { File } from '@kbn/files-plugin/common'; + +import type { Case } from '../../../common'; +import { MAX_USER_ACTIONS_PER_CASE } from '../../../common/constants'; +import { createUserActionServiceMock } from '../../services/mocks'; +import { createCasesClientMock, createCasesClientMockArgs } from '../mocks'; +import { addFile } from './add_file'; +import { buildAttachmentRequestFromFileJSON } from '../utils'; + +jest.mock('../utils'); + +const buildAttachmentRequestFromFileJSONMock = buildAttachmentRequestFromFileJSON as jest.Mock; + +describe('addFile', () => { + const caseId = 'test-case'; + const file = new Readable(); + file.push('theFile'); + file.push(null); + const owner = 'cases'; + + const clientArgs = createCasesClientMockArgs(); + const casesClient = createCasesClientMock(); + const userActionService = createUserActionServiceMock(); + + clientArgs.services.userActionService = userActionService; + + beforeEach(() => { + jest.clearAllMocks(); + userActionService.getMultipleCasesUserActionsTotal.mockResolvedValue({}); + casesClient.cases.get.mockResolvedValue({ id: caseId, owner } as unknown as Case); + }); + + it('throws an error if the filename is missing', async () => { + await expect( + addFile( + // @ts-expect-error + { caseId, fileRequest: { file, mimeType: 'text/plain' } }, + clientArgs, + casesClient + ) + ).rejects.toThrow(`Invalid value "undefined" supplied to "filename"`); + }); + + it('throws an error if the mimeType is not part of the allowed mime types', async () => { + await expect( + addFile({ caseId, file, filename: 'foobar', mimeType: 'foo/bar' }, clientArgs, casesClient) + ).rejects.toThrow('The mime type field value foo/bar is not allowed.'); + }); + + it(`throws an error when the case user actions become > ${MAX_USER_ACTIONS_PER_CASE}`, async () => { + userActionService.getMultipleCasesUserActionsTotal.mockResolvedValue({ + [caseId]: MAX_USER_ACTIONS_PER_CASE, + }); + + await expect( + addFile({ caseId, file, filename: 'foobar', mimeType: 'text/plain' }, clientArgs, casesClient) + ).rejects.toThrow( + `The case with id ${caseId} has reached the limit of ${MAX_USER_ACTIONS_PER_CASE} user actions.` + ); + }); + + it('calls fileService.delete when an error is thrown after the file was created', async () => { + const id = 'file-id'; + + clientArgs.fileService.create.mockResolvedValue({ + id, + uploadContent: jest.fn(), + toJSON: () => { + throw new Error(); // ensures an error is thrown after file creation + }, + } as unknown as File); + + await expect( + addFile( + { + caseId, + file, + filename: 'foobar', + mimeType: 'text/plain', + }, + clientArgs, + casesClient + ) + ).rejects.toThrowError(); + expect(clientArgs.fileService.delete).toHaveBeenCalledWith({ id }); + }); + + it('calls buildAttachmentRequestFromFileJSON with the correct params', async () => { + const fileMetadata = { + id: 'file-id', + created: 'created', + extension: 'jpg', + mimeType: 'image/jpeg', + name: 'foobar', + }; + + clientArgs.fileService.create.mockResolvedValue({ + id: fileMetadata.id, + uploadContent: jest.fn(), + toJSON: () => fileMetadata, + } as unknown as File); + + buildAttachmentRequestFromFileJSONMock.mockImplementation(() => { + throw new Error(); // ensures the test finishes early + }); + + await expect( + addFile( + { + caseId, + file, + filename: fileMetadata.name, + mimeType: fileMetadata.mimeType, + }, + clientArgs, + casesClient + ) + ).rejects.toThrowError(); + expect(buildAttachmentRequestFromFileJSONMock).toHaveBeenCalledWith({ owner, fileMetadata }); + }); +}); diff --git a/x-pack/plugins/cases/server/client/attachments/add_file.ts b/x-pack/plugins/cases/server/client/attachments/add_file.ts new file mode 100644 index 0000000000000..5f3ee614e99ed --- /dev/null +++ b/x-pack/plugins/cases/server/client/attachments/add_file.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectsUtils } from '@kbn/core/server'; + +import type { Owner } from '../../../common/constants/types'; +import { FileAttachmentMetadataPayloadRt, type Case } from '../../../common/types/domain'; +import type { CasesClient, CasesClientArgs } from '..'; +import type { AddFileArgs } from './types'; + +import { CaseCommentModel } from '../../common/models'; +import { createCaseError } from '../../common/error'; +import { validateMaxUserActions } from '../../common/validators'; +import { constructFileKindIdByOwner } from '../../../common/files'; +import { Operations } from '../../authorization'; +import { validateRegisteredAttachments } from './validators'; +import { buildAttachmentRequestFromFileJSON } from '../utils'; +import { decodeWithExcessOrThrow } from '../../common/runtime_types'; + +/** + * Create a file attachment to a case. + */ +export const addFile = async ( + addFileArgs: AddFileArgs, + clientArgs: CasesClientArgs, + casesClient: CasesClient +): Promise => { + const { caseId, file, filename, mimeType, $abort } = addFileArgs; + const { + logger, + authorization, + persistableStateAttachmentTypeRegistry, + externalReferenceAttachmentTypeRegistry, + services: { userActionService }, + fileService, + } = clientArgs; + + let createdFile; + + try { + decodeWithExcessOrThrow(FileAttachmentMetadataPayloadRt)({ + filename, + mimeType, + }); + + // This will perform an authorization check to ensure the user has access to the parent case + const theCase = await casesClient.cases.get({ + id: caseId, + includeComments: false, + }); + + const owner = theCase.owner; + + await validateMaxUserActions({ caseId, userActionService, userActionsToAdd: 1 }); + + const savedObjectID = SavedObjectsUtils.generateId(); + + await authorization.ensureAuthorized({ + operation: Operations.createComment, + entities: [{ owner, id: savedObjectID }], + }); + + createdFile = await fileService.create({ + name: filename, + mime: mimeType, + fileKind: constructFileKindIdByOwner(owner as Owner), + meta: { caseIds: [caseId], owner: [owner] }, + }); + + await createdFile.uploadContent(file, $abort); + + const commentReq = buildAttachmentRequestFromFileJSON({ + owner, + fileMetadata: createdFile.toJSON(), + }); + + validateRegisteredAttachments({ + query: commentReq, + persistableStateAttachmentTypeRegistry, + externalReferenceAttachmentTypeRegistry, + }); + + const createdDate = new Date().toISOString(); + + const model = await CaseCommentModel.create(caseId, clientArgs); + + const updatedModel = await model.createComment({ + createdDate, + commentReq, + id: savedObjectID, + }); + + return await updatedModel.encodeWithComments(); + } catch (error) { + if (createdFile?.id) { + await fileService.delete({ id: createdFile.id }); + } + + throw createCaseError({ + message: `Failed while adding a comment to case id: ${caseId} error: ${error}`, + error, + logger, + }); + } +}; diff --git a/x-pack/plugins/cases/server/client/attachments/client.ts b/x-pack/plugins/cases/server/client/attachments/client.ts index 9c455cbe750c3..61586092e69b9 100644 --- a/x-pack/plugins/cases/server/client/attachments/client.ts +++ b/x-pack/plugins/cases/server/client/attachments/client.ts @@ -28,6 +28,7 @@ import type { UpdateArgs, BulkGetArgs, BulkDeleteFileArgs, + AddFileArgs, } from './types'; import { bulkCreate } from './bulk_create'; import { deleteAll, deleteComment } from './delete'; @@ -35,6 +36,7 @@ import { find, get, getAll, getAllAlertsAttachToCase } from './get'; import { bulkGet } from './bulk_get'; import { update } from './update'; import { bulkDeleteFileAttachments } from './bulk_delete'; +import { addFile } from './add_file'; /** * API for interacting with the attachments to a case. @@ -77,6 +79,10 @@ export interface AttachmentsSubClient { * The request must include all fields for the attachment. Even the fields that are not changing. */ update(updateArgs: UpdateArgs): Promise; + /** + * Adds a file attachment to a case. Returns the case with comments. + */ + addFile(params: AddFileArgs): Promise; } /** @@ -102,6 +108,7 @@ export const createAttachmentsSubClient = ( getAll: (params) => getAll(params, clientArgs), get: (params) => get(params, clientArgs), update: (params) => update(params, clientArgs), + addFile: (params: AddFileArgs) => addFile(params, clientArgs, casesClient), }; return Object.freeze(attachmentSubClient); diff --git a/x-pack/plugins/cases/server/client/attachments/types.ts b/x-pack/plugins/cases/server/client/attachments/types.ts index 0dd700a813110..bc9abed0d0da0 100644 --- a/x-pack/plugins/cases/server/client/attachments/types.ts +++ b/x-pack/plugins/cases/server/client/attachments/types.ts @@ -4,7 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { Readable } from 'stream'; +import type { ReplaySubject } from 'rxjs'; import type { BulkCreateAttachmentsRequest, AttachmentPatchRequest, @@ -132,3 +133,36 @@ export interface UpdateArgs { */ updateRequest: AttachmentPatchRequest; } + +export interface HapiReadableStream extends Readable { + hapi: { + filename: string; + headers: Record; + }; +} + +/** + * The arguments needed for attaching a file to a case. + */ +export interface AddFileArgs { + /** + * The case ID that this attachment will be associated with + */ + caseId: string; + /** + * The file to upload + */ + file: Readable; + /** + * The name of the file to upload + */ + filename: string; + /** + * The mime type of the file to upload + */ + mimeType?: string; + /** + * An observable that can be used to abort the upload at any time. + */ + $abort?: ReplaySubject; +} diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index 74d3c3de46fa0..50dca1920b625 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -91,6 +91,7 @@ const createAttachmentsSubClientMock = (): AttachmentsSubClientMock => { return { bulkGet: jest.fn(), add: jest.fn(), + addFile: jest.fn(), bulkCreate: jest.fn(), delete: jest.fn(), deleteAll: jest.fn(), diff --git a/x-pack/plugins/cases/server/client/utils.test.ts b/x-pack/plugins/cases/server/client/utils.test.ts index 680887b82c653..7b5d692f6aa79 100644 --- a/x-pack/plugins/cases/server/client/utils.test.ts +++ b/x-pack/plugins/cases/server/client/utils.test.ts @@ -14,6 +14,7 @@ import { toElasticsearchQuery, toKqlExpression } from '@kbn/es-query'; import { createSavedObjectsSerializerMock } from './mocks'; import { arraysDifference, + buildAttachmentRequestFromFileJSON, buildFilter, buildRangeFilter, constructQueryOptions, @@ -24,6 +25,7 @@ import { import { CasePersistedSeverity, CasePersistedStatus } from '../common/types/case'; import type { CustomFieldsConfiguration } from '../../common/types/domain'; import { CaseSeverity, CaseStatuses, CustomFieldTypes } from '../../common/types/domain'; +import type { FileJSON } from '@kbn/shared-ux-file-types'; describe('utils', () => { describe('buildFilter', () => { @@ -1576,4 +1578,40 @@ describe('utils', () => { expect(res).toEqual([]); }); }); + + describe('buildAttachmentRequestFromFileJSON', () => { + it('builds attachment request correctly', () => { + expect( + buildAttachmentRequestFromFileJSON({ + owner: 'theOwner', + fileMetadata: { + id: 'file-id', + created: 'created', + extension: 'jpg', + mimeType: 'image/jpeg', + name: 'foobar', + } as FileJSON, + }) + ).toStrictEqual({ + externalReferenceAttachmentTypeId: '.files', + externalReferenceId: 'file-id', + externalReferenceMetadata: { + files: [ + { + created: 'created', + extension: 'jpg', + mimeType: 'image/jpeg', + name: 'foobar', + }, + ], + }, + externalReferenceStorage: { + soType: 'file', + type: 'savedObject', + }, + owner: 'theOwner', + type: 'externalReference', + }); + }); + }); }); diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index 5f854aa3236fe..6447b53abd00a 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -16,6 +16,8 @@ import type { KueryNode } from '@kbn/es-query'; import { nodeBuilder, fromKueryExpression, escapeKuery } from '@kbn/es-query'; import { spaceIdToNamespace } from '@kbn/spaces-plugin/server/lib/utils/namespace'; +import type { FileJSON } from '@kbn/shared-ux-file-types'; +import { FILE_SO_TYPE } from '@kbn/files-plugin/common/constants'; import type { CaseCustomField, CaseSeverity, @@ -28,6 +30,7 @@ import type { import { ActionsAttachmentPayloadRt, AlertAttachmentPayloadRt, + AttachmentType, ExternalReferenceNoSOAttachmentPayloadRt, ExternalReferenceSOAttachmentPayloadRt, ExternalReferenceStorageType, @@ -40,6 +43,7 @@ import type { CasesSearchParams } from './types'; import { decodeWithExcessOrThrow } from '../common/runtime_types'; import { CASE_SAVED_OBJECT, + FILE_ATTACHMENT_TYPE, NO_ASSIGNEES_FILTERING_KEYWORD, OWNER_FIELD, } from '../../common/constants'; @@ -660,3 +664,30 @@ export const transformTemplateCustomFields = ({ }; }); }; + +export const buildAttachmentRequestFromFileJSON = ({ + owner, + fileMetadata, +}: { + owner: string; + fileMetadata: FileJSON; +}): AttachmentRequest => ({ + owner, + type: AttachmentType.externalReference, + externalReferenceId: fileMetadata.id, + externalReferenceStorage: { + type: ExternalReferenceStorageType.savedObject, + soType: FILE_SO_TYPE, + }, + externalReferenceAttachmentTypeId: FILE_ATTACHMENT_TYPE, + externalReferenceMetadata: { + files: [ + { + name: fileMetadata.name, + extension: fileMetadata.extension ?? 'txt', + mimeType: fileMetadata.mimeType ?? 'text/plain', + created: fileMetadata.created, + }, + ], + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/files/post_file.test.ts b/x-pack/plugins/cases/server/routes/api/files/post_file.test.ts new file mode 100644 index 0000000000000..5fb2eb581320e --- /dev/null +++ b/x-pack/plugins/cases/server/routes/api/files/post_file.test.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createCasesClientMock } from '../../../client/mocks'; +import { postFileRoute } from './post_file'; + +describe('getCaseRoute', () => { + const casesClientMock = createCasesClientMock(); + const response = { ok: jest.fn() }; + const context = { cases: { getCasesClient: jest.fn().mockResolvedValue(casesClientMock) } }; + const sub = { unsubscribe: jest.fn() }; + + afterEach(() => jest.clearAllMocks()); + + it('extracts the file metadata from hapi as expected', async () => { + const request = { + body: { file: { hapi: { filename: 'foobar.txt' } } }, + events: { aborted$: { subscribe: jest.fn().mockReturnValue(sub) } }, + params: { case_id: 'bar' }, + }; + + // @ts-ignore + await postFileRoute.handler({ context, request, response }); + + expect(casesClientMock.attachments.addFile).toHaveBeenCalledWith( + expect.objectContaining({ + caseId: 'bar', + file: { + hapi: { + filename: 'foobar.txt', + }, + }, + filename: 'foobar', + mimeType: 'text/plain', + }) + ); + + expect(sub.unsubscribe).toHaveBeenCalled(); + }); + + it('filename in body takes precedence over metadata', async () => { + const request = { + body: { file: { hapi: { filename: 'foobar.txt' } }, filename: 'foo' }, + events: { aborted$: { subscribe: jest.fn().mockReturnValue(sub) } }, + params: { case_id: 'bar' }, + }; + + // @ts-ignore + await postFileRoute.handler({ context, request, response }); + + expect(casesClientMock.attachments.addFile).toHaveBeenCalledWith( + expect.objectContaining({ + caseId: 'bar', + file: { + hapi: { + filename: 'foobar.txt', + }, + }, + filename: 'foo', + mimeType: 'text/plain', + }) + ); + + expect(sub.unsubscribe).toHaveBeenCalled(); + }); + + it('unrecognized mimetype will be sent as undefined', async () => { + const request = { + body: { file: { hapi: { filename: 'foobar.foobar' } } }, + events: { aborted$: { subscribe: jest.fn().mockReturnValue(sub) } }, + params: { case_id: 'bar' }, + }; + + // @ts-ignore + await postFileRoute.handler({ context, request, response }); + + expect(casesClientMock.attachments.addFile).toHaveBeenCalledWith( + expect.objectContaining({ + caseId: 'bar', + file: { + hapi: { + filename: 'foobar.foobar', + }, + }, + }) + ); + + expect(sub.unsubscribe).toHaveBeenCalled(); + }); + + it('missing hapi will not throw an error', async () => { + const request = { + body: { file: {} }, + events: { aborted$: { subscribe: jest.fn().mockReturnValue(sub) } }, + params: { case_id: 'bar' }, + }; + + // @ts-ignore + await postFileRoute.handler({ context, request, response }); + + expect(casesClientMock.attachments.addFile).toHaveBeenCalledWith( + expect.objectContaining({ + caseId: 'bar', + file: {}, + filename: '', + }) + ); + + expect(sub.unsubscribe).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/cases/server/routes/api/files/post_file.ts b/x-pack/plugins/cases/server/routes/api/files/post_file.ts new file mode 100644 index 0000000000000..2d7b5b6318a4b --- /dev/null +++ b/x-pack/plugins/cases/server/routes/api/files/post_file.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; +import mime from 'mime-types'; +import { parse } from 'path'; + +import { schema } from '@kbn/config-schema'; +import { ReplaySubject } from 'rxjs'; + +import { AbortedUploadError } from '@kbn/files-plugin/server/file/errors'; +import type { HapiReadableStream } from '../../../client/attachments/types'; +import type { attachmentApiV1 } from '../../../../common/types/api'; + +import { CASE_FILES_URL } from '../../../../common/constants'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; +import type { caseDomainV1 } from '../../../../common/types/domain'; + +export const postFileRoute = createCasesRoute({ + method: 'post', + path: CASE_FILES_URL, + params: { + params: schema.object({ + case_id: schema.string(), + }), + }, + routerOptions: { + access: 'public', + summary: 'Attach a file to a case', + tags: ['oas-tag:cases'], + body: { + // This is set to 10 GiB as an upper boundary on the size of the HTTP request body. + // The file service will throw 413 errors if the file size is larger than expected. + maxBytes: 10 * 1024 * 1024 * 1024, + output: 'stream', + parse: true, + accepts: 'multipart/form-data', + }, + }, + handler: async ({ context, request, response }) => { + const $abort = new ReplaySubject(); + const sub = request.events.aborted$.subscribe($abort); + + try { + const caseContext = await context.cases; + const casesClient = await caseContext.getCasesClient(); + + const fileRequest = request.body as attachmentApiV1.PostFileAttachmentRequest; + const file = fileRequest.file as HapiReadableStream; + + let filename = fileRequest.filename; + let mimeType; + + if (file.hapi != null) { + const parsedFilename = parse(file.hapi.filename); + + filename ??= parsedFilename.name; + mimeType = mime.lookup(parsedFilename.ext.toLowerCase()) || undefined; + } + + const res: caseDomainV1.Case = await casesClient.attachments.addFile({ + file, + filename: filename ?? '', + mimeType, + caseId: request.params.case_id, + $abort, + }); + + return response.ok({ + body: res, + }); + } catch (error) { + if (error instanceof AbortedUploadError) { + throw new Boom.Boom(error, { + statusCode: 499, + message: error.message, + }); + } + throw createCaseError({ + message: `Failed to attach file to case in route: ${error}`, + error, + }); + } finally { + sub.unsubscribe(); + } + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/get_external_routes.ts b/x-pack/plugins/cases/server/routes/api/get_external_routes.ts index 4412d0b695079..b68a1c32a93f8 100644 --- a/x-pack/plugins/cases/server/routes/api/get_external_routes.ts +++ b/x-pack/plugins/cases/server/routes/api/get_external_routes.ts @@ -30,6 +30,7 @@ import { patchCaseConfigureRoute } from './configure/patch_configure'; import { postCaseConfigureRoute } from './configure/post_configure'; import { getAllAlertsAttachedToCaseRoute } from './comments/get_alerts'; import { findUserActionsRoute } from './user_actions/find_user_actions'; +import { postFileRoute } from './files/post_file'; export const getExternalRoutes = ({ isServerless }: { isServerless?: boolean }) => [ @@ -58,4 +59,5 @@ export const getExternalRoutes = ({ isServerless }: { isServerless?: boolean }) patchCaseConfigureRoute, postCaseConfigureRoute, getAllAlertsAttachedToCaseRoute, + postFileRoute, ] as CaseRoute[]; diff --git a/x-pack/plugins/embeddable_enhanced/public/plugin.ts b/x-pack/plugins/embeddable_enhanced/public/plugin.ts index f05982af78947..a76f33f095951 100644 --- a/x-pack/plugins/embeddable_enhanced/public/plugin.ts +++ b/x-pack/plugins/embeddable_enhanced/public/plugin.ts @@ -148,8 +148,8 @@ export class EmbeddableEnhancedPlugin ); const api: DynamicActionStorageApi = { dynamicActionsState$, - setDynamicActions: (newState) => { - dynamicActionsState$.next(newState); + setDynamicActions: (enhancements) => { + dynamicActionsState$.next(getDynamicActionsState(enhancements)); }, }; const storage = new DynamicActionStorage(uuid, getTitle, api); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx index a27fb82fb82b0..7ba6832b8833d 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx @@ -52,6 +52,15 @@ jest.mock('@elastic/eui', () => { }} /> ), + EuiSuperSelect: (props: any) => ( + { + props.onChange(e.target.value); + }} + /> + ), }; }); @@ -301,6 +310,15 @@ describe('', () => { expect(find('stepTitle').text()).toEqual('Index settings (optional)'); }); + it('should display a warning callout displaying the selected index mode', async () => { + const { exists, find } = testBed; + + expect(exists('indexModeCallout')).toBe(true); + expect(find('indexModeCallout').text()).toContain( + 'The index.mode setting has been set to Standard within template Logistics.' + ); + }); + it('should not allow invalid json', async () => { const { form, actions } = testBed; @@ -426,6 +444,53 @@ describe('', () => { }); }); + describe('logistics (step 1)', () => { + beforeEach(async () => { + await act(async () => { + testBed = await setup(httpSetup); + }); + testBed.component.update(); + }); + + it('setting index pattern to logs-*-* should set the index mode to logsdb', async () => { + const { component, actions } = testBed; + // Logistics + await actions.completeStepOne({ name: 'my_logs_template', indexPatterns: ['logs-*-*'] }); + // Component templates + await actions.completeStepTwo(); + // Index settings + await actions.completeStepThree('{}'); + // Mappings + await actions.completeStepFour(); + // Aliases + await actions.completeStepFive(); + + await act(async () => { + actions.clickNextButton(); + }); + component.update(); + + expect(httpSetup.post).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/index_templates`, + expect.objectContaining({ + body: JSON.stringify({ + name: 'my_logs_template', + indexPatterns: ['logs-*-*'], + allowAutoCreate: 'NO_OVERWRITE', + indexMode: 'logsdb', + dataStream: {}, + _kbnMeta: { + type: 'default', + hasDatastream: false, + isLegacy: false, + }, + template: {}, + }), + }) + ); + }); + }); + describe('review (step 6)', () => { beforeEach(async () => { await act(async () => { @@ -529,6 +594,7 @@ describe('', () => { name: TEMPLATE_NAME, indexPatterns: DEFAULT_INDEX_PATTERNS, allowAutoCreate: 'TRUE', + indexMode: 'time_series', }); // Component templates await actions.completeStepTwo('test_component_template_1'); @@ -557,6 +623,7 @@ describe('', () => { name: TEMPLATE_NAME, indexPatterns: DEFAULT_INDEX_PATTERNS, allowAutoCreate: 'TRUE', + indexMode: 'time_series', dataStream: {}, _kbnMeta: { type: 'default', diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx index dac7dadfa2557..bbd1d24c7906d 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx @@ -169,6 +169,7 @@ describe('', () => { indexPatterns: ['myPattern*'], version: 1, allowAutoCreate: 'NO_OVERWRITE', + indexMode: 'standard', dataStream: { hidden: true, anyUnknownKey: 'should_be_kept', diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts index 7977c4373d765..7c02608d4e3f7 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts @@ -148,6 +148,7 @@ export const formSetup = async (initTestBed: SetupFunc) => { enableDataStream, lifecycle, allowAutoCreate, + indexMode, }: Partial & { enableDataStream?: boolean } = {}) => { const { component, form, find } = testBed; @@ -204,6 +205,10 @@ export const formSetup = async (initTestBed: SetupFunc) => { radioOption.simulate('change', { target: { checked: true } }); component.update(); } + + if (indexMode) { + form.setSelectValue('indexModeField', indexMode); + } }); component.update(); @@ -356,6 +361,7 @@ export type TestSubjects = | 'mappingsEditorFieldEdit' | 'mockCodeEditor' | 'mockComboBox' + | 'mockSuperSelect' | 'nameField' | 'nameField.input' | 'nameParameterInput' @@ -364,6 +370,8 @@ export type TestSubjects = | 'orderField.input' | 'priorityField.input' | 'dataStreamField.input' + | 'indexModeField' + | 'indexModeCallout' | 'dataRetentionToggle.input' | 'allowAutoCreateField.input' | 'pageTitle' diff --git a/x-pack/plugins/index_management/common/constants/index.ts b/x-pack/plugins/index_management/common/constants/index.ts index 49e2a7f9505a9..81e50fa8b75be 100644 --- a/x-pack/plugins/index_management/common/constants/index.ts +++ b/x-pack/plugins/index_management/common/constants/index.ts @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; export { BASE_PATH } from './base_path'; export { API_BASE_PATH, INTERNAL_API_BASE_PATH } from './api_base_path'; export { INVALID_INDEX_PATTERN_CHARS, INVALID_TEMPLATE_NAME_CHARS } from './invalid_characters'; +export * from './index_modes'; export * from './index_statuses'; // Since each index can have a max length or 255 characters and the max length of diff --git a/x-pack/plugins/index_management/common/constants/index_modes.ts b/x-pack/plugins/index_management/common/constants/index_modes.ts new file mode 100644 index 0000000000000..65099cf4e4f8b --- /dev/null +++ b/x-pack/plugins/index_management/common/constants/index_modes.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const STANDARD_INDEX_MODE = 'standard'; +export const LOGSDB_INDEX_MODE = 'logsdb'; +export const TIME_SERIES_MODE = 'time_series'; diff --git a/x-pack/plugins/index_management/common/lib/template_serialization.test.ts b/x-pack/plugins/index_management/common/lib/template_serialization.test.ts index 8f9f73c334a9f..cb86de2660fd3 100644 --- a/x-pack/plugins/index_management/common/lib/template_serialization.test.ts +++ b/x-pack/plugins/index_management/common/lib/template_serialization.test.ts @@ -6,7 +6,8 @@ */ import { deserializeTemplate, serializeTemplate } from './template_serialization'; -import { TemplateDeserialized, TemplateSerialized } from '../types'; +import { TemplateDeserialized, TemplateSerialized, IndexMode } from '../types'; +import { STANDARD_INDEX_MODE, LOGSDB_INDEX_MODE, TIME_SERIES_MODE } from '../constants'; const defaultSerializedTemplate: TemplateSerialized = { template: {}, @@ -17,6 +18,7 @@ const defaultSerializedTemplate: TemplateSerialized = { const defaultDeserializedTemplate: TemplateDeserialized = { name: 'my_template', indexPatterns: ['test'], + indexMode: STANDARD_INDEX_MODE, _kbnMeta: { type: 'default', hasDatastream: true, @@ -26,12 +28,13 @@ const defaultDeserializedTemplate: TemplateDeserialized = { const allowAutoCreateRadioOptions = ['NO_OVERWRITE', 'TRUE', 'FALSE']; const allowAutoCreateSerializedValues = [undefined, true, false]; +const indexModeValues = [STANDARD_INDEX_MODE, LOGSDB_INDEX_MODE, TIME_SERIES_MODE, undefined]; describe('Template serialization', () => { describe('serialization of allow_auto_create parameter', () => { describe('deserializeTemplate()', () => { allowAutoCreateSerializedValues.forEach((value, index) => { - test(`correctly deserializes ${value} value`, () => { + test(`correctly deserializes ${value} allow_auto_create value`, () => { expect( deserializeTemplate({ ...defaultSerializedTemplate, @@ -41,11 +44,29 @@ describe('Template serialization', () => { ).toHaveProperty('allowAutoCreate', allowAutoCreateRadioOptions[index]); }); }); + + indexModeValues.forEach((value) => { + test(`correctly deserializes ${value} index mode settings value`, () => { + expect( + deserializeTemplate({ + ...defaultSerializedTemplate, + name: 'my_template', + template: { + settings: { + index: { + mode: value, + }, + }, + }, + }) + ).toHaveProperty('indexMode', value ?? STANDARD_INDEX_MODE); + }); + }); }); describe('serializeTemplate()', () => { allowAutoCreateRadioOptions.forEach((option, index) => { - test(`correctly serializes ${option} radio option`, () => { + test(`correctly serializes ${option} allowAutoCreate radio option`, () => { expect( serializeTemplate({ ...defaultDeserializedTemplate, @@ -54,6 +75,18 @@ describe('Template serialization', () => { ).toHaveProperty('allow_auto_create', allowAutoCreateSerializedValues[index]); }); }); + + // Only use the first three values (omit undefined) + indexModeValues.slice(0, 3).forEach((value) => { + test(`correctly serializes ${value} indexMode option`, () => { + expect( + serializeTemplate({ + ...defaultDeserializedTemplate, + indexMode: value as IndexMode, + }) + ).toHaveProperty('template.settings.index.mode', value); + }); + }); }); }); }); diff --git a/x-pack/plugins/index_management/common/lib/template_serialization.ts b/x-pack/plugins/index_management/common/lib/template_serialization.ts index 0ed52e3f04ba0..999023704559c 100644 --- a/x-pack/plugins/index_management/common/lib/template_serialization.ts +++ b/x-pack/plugins/index_management/common/lib/template_serialization.ts @@ -11,9 +11,15 @@ import { TemplateSerialized, TemplateListItem, TemplateType, + IndexMode, } from '../types'; import { deserializeESLifecycle } from './data_stream_utils'; -import { allowAutoCreateRadioValues, allowAutoCreateRadioIds } from '../constants'; +import { + allowAutoCreateRadioValues, + allowAutoCreateRadioIds, + STANDARD_INDEX_MODE, + LOGSDB_INDEX_MODE, +} from '../constants'; const hasEntries = (data: object = {}) => Object.entries(data).length > 0; @@ -26,6 +32,7 @@ export function serializeTemplate(templateDeserialized: TemplateDeserialized): T composedOf, ignoreMissingComponentTemplates, dataStream, + indexMode, _meta, allowAutoCreate, deprecated, @@ -34,7 +41,16 @@ export function serializeTemplate(templateDeserialized: TemplateDeserialized): T return { version, priority, - template, + template: { + ...template, + settings: { + ...template?.settings, + index: { + ...template?.settings?.index, + mode: indexMode, + }, + }, + }, index_patterns: indexPatterns, data_stream: dataStream, composed_of: composedOf, @@ -75,6 +91,11 @@ export function deserializeTemplate( const ilmPolicyName = settings?.index?.lifecycle?.name; + const indexMode = (settings?.index?.mode ?? + (indexPatterns.some((pattern) => pattern === 'logs-*-*') + ? LOGSDB_INDEX_MODE + : STANDARD_INDEX_MODE)) as IndexMode; + const deserializedTemplate: TemplateDeserialized = { name, version, @@ -82,6 +103,7 @@ export function deserializeTemplate( ...(template.lifecycle ? { lifecycle: deserializeESLifecycle(template.lifecycle) } : {}), indexPatterns: indexPatterns.sort(), template, + indexMode, ilmPolicy: ilmPolicyName ? { name: ilmPolicyName } : undefined, composedOf: composedOf ?? [], ignoreMissingComponentTemplates: ignoreMissingComponentTemplates ?? [], diff --git a/x-pack/plugins/index_management/common/types/index.ts b/x-pack/plugins/index_management/common/types/index.ts index ef2e8a389c079..7ec100bc1d366 100644 --- a/x-pack/plugins/index_management/common/types/index.ts +++ b/x-pack/plugins/index_management/common/types/index.ts @@ -19,6 +19,7 @@ export type { DataStream, DataStreamIndex, DataRetention, + IndexMode, } from './data_streams'; export * from './component_templates'; diff --git a/x-pack/plugins/index_management/common/types/templates.ts b/x-pack/plugins/index_management/common/types/templates.ts index b05a29a961a73..ab4614200c0b5 100644 --- a/x-pack/plugins/index_management/common/types/templates.ts +++ b/x-pack/plugins/index_management/common/types/templates.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DataRetention, DataStream } from './data_streams'; +import { DataRetention, DataStream, IndexMode } from './data_streams'; import { IndexSettings } from './indices'; import { Aliases } from './aliases'; import { Mappings } from './mappings'; @@ -51,6 +51,7 @@ export interface TemplateDeserialized { priority?: number; // Composable template only allowAutoCreate: string; order?: number; // Legacy template only + indexMode: IndexMode; ilmPolicy?: { name: string; }; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings.tsx index 9b26ed38223c4..38a11f03c7ee6 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings.tsx @@ -16,6 +16,8 @@ import { EuiFormRow, EuiText, EuiCode, + EuiCallOut, + EuiLink, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { CodeEditor } from '@kbn/code-editor'; @@ -23,15 +25,19 @@ import { CodeEditor } from '@kbn/code-editor'; import { Forms } from '../../../../../shared_imports'; import { useJsonStep } from './use_json_step'; import { documentationService } from '../../../mappings_editor/shared_imports'; +import { indexModeLabels } from '../../../../lib/index_mode_labels'; +import { IndexMode } from '../../../../../../common/types'; interface Props { onChange: (content: Forms.Content) => void; esDocsBase: string; defaultValue?: { [key: string]: any }; + indexMode?: IndexMode; } export const StepSettings: React.FunctionComponent = React.memo( - ({ defaultValue = {}, onChange, esDocsBase }) => { + ({ defaultValue = {}, onChange, esDocsBase, indexMode }) => { + const { navigateToStep } = Forms.useFormWizardContext(); const { jsonContent, setJsonContent, error } = useJsonStep({ defaultValue, onChange, @@ -80,6 +86,47 @@ export const StepSettings: React.FunctionComponent = React.memo( + {indexMode && ( + <> + + {i18n.translate( + 'xpack.idxMgmt.formWizard.stepSettings.indexModeCallout.indexModeSettingLabel', + { + defaultMessage: 'index.mode', + } + )} + + ), + indexMode: indexModeLabels[indexMode], + logisticsLink: ( + navigateToStep(0)}> + {i18n.translate( + 'xpack.idxMgmt.formWizard.stepSettings.indexModeCallout.logisticsLinkLabel', + { + defaultMessage: 'Logistics', + } + )} + + ), + }} + /> + } + color="warning" + iconType="warning" + data-test-subj="indexModeCallout" + /> + + + + )} + {/* Settings code editor */} TemplateDeserialized; } -export const StepSettingsContainer = React.memo(({ esDocsBase }: Props) => { +export const StepSettingsContainer = React.memo(({ esDocsBase, getTemplateData }: Props) => { const { defaultValue, updateContent } = Forms.useContent( 'settings' ); + const { getData } = Forms.useMultiContentContext(); + + let indexMode; + if (getTemplateData) { + const wizardContent = getData(); + // Build the current template object, providing the wizard content data + const template = getTemplateData(wizardContent); + indexMode = template.indexMode; + } return ( - + ); }); diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx index d73d95600e5b1..9742f2eec525f 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect, useCallback } from 'react'; +import React, { useEffect, useCallback, Fragment } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -14,6 +14,7 @@ import { EuiSpacer, EuiLink, EuiCode, + EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; @@ -34,7 +35,13 @@ import { UnitField, timeUnits } from '../../shared'; import { DataRetention } from '../../../../../common'; import { documentationService } from '../../../services/documentation'; import { schemas, nameConfig, nameConfigWithoutValidations } from '../template_form_schemas'; -import { allowAutoCreateRadios } from '../../../../../common/constants'; +import { + allowAutoCreateRadios, + STANDARD_INDEX_MODE, + TIME_SERIES_MODE, + LOGSDB_INDEX_MODE, +} from '../../../../../common/constants'; +import { indexModeLabels, indexModeDescriptions } from '../../../lib/index_mode_labels'; // Create or Form components with partial props that are common to all instances const UseField = getUseField({ component: Field }); @@ -91,6 +98,54 @@ function getFieldsMeta(esDocsBase: string) { ), testSubject: 'dataStreamField', }, + indexMode: { + title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.indexModeTitle', { + defaultMessage: 'Data stream index mode', + }), + description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.indexModeDescription', { + defaultMessage: + 'The index.mode setting is used to control settings applied in specific domains like ingestions of time series data or logs.', + }), + options: [ + { + value: STANDARD_INDEX_MODE, + inputDisplay: indexModeLabels[STANDARD_INDEX_MODE], + dropdownDisplay: ( + + {indexModeLabels[STANDARD_INDEX_MODE]} + +

{indexModeDescriptions[STANDARD_INDEX_MODE]}

+
+
+ ), + }, + { + value: TIME_SERIES_MODE, + inputDisplay: indexModeLabels[TIME_SERIES_MODE], + dropdownDisplay: ( + + {indexModeLabels[TIME_SERIES_MODE]} + +

{indexModeDescriptions[TIME_SERIES_MODE]}

+
+
+ ), + }, + { + value: LOGSDB_INDEX_MODE, + inputDisplay: indexModeLabels[LOGSDB_INDEX_MODE], + dropdownDisplay: ( + + {indexModeLabels[LOGSDB_INDEX_MODE]} + +

{indexModeDescriptions[LOGSDB_INDEX_MODE]}

+
+
+ ), + }, + ], + testSubject: 'indexModeField', + }, order: { title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.orderTitle', { defaultMessage: 'Merge order', @@ -198,21 +253,37 @@ export const StepLogistics: React.FunctionComponent = React.memo( isValid: isFormValid, getErrors: getFormErrors, getFormData, + setFieldValue, } = form; - const [{ addMeta, doCreateDataStream, lifecycle }] = useFormData<{ - addMeta: boolean; - lifecycle: DataRetention; - doCreateDataStream: boolean; - }>({ - form, - watch: [ - 'addMeta', - 'lifecycle.enabled', - 'lifecycle.infiniteDataRetention', - 'doCreateDataStream', - ], - }); + const [{ addMeta, doCreateDataStream, lifecycle, indexPatterns: indexPatternsField }] = + useFormData<{ + addMeta: boolean; + lifecycle: DataRetention; + doCreateDataStream: boolean; + indexPatterns: string[]; + }>({ + form, + watch: [ + 'addMeta', + 'lifecycle.enabled', + 'lifecycle.infiniteDataRetention', + 'doCreateDataStream', + 'indexPatterns', + ], + }); + + useEffect(() => { + if ( + indexPatternsField && + indexPatternsField.length === 1 && + indexPatternsField[0] === 'logs-*-*' && + // Only set index mode if index pattern was changed + defaultValue.indexPatterns !== indexPatternsField + ) { + setFieldValue('indexMode', LOGSDB_INDEX_MODE); + } + }, [defaultValue.indexPatterns, indexPatternsField, setFieldValue]); /** * When the consumer call validate() on this step, we submit the form so it enters the "isSubmitted" state @@ -234,6 +305,7 @@ export const StepLogistics: React.FunctionComponent = React.memo( name, indexPatterns, createDataStream, + indexMode, order, priority, version, @@ -312,6 +384,21 @@ export const StepLogistics: React.FunctionComponent = React.memo( )} + {doCreateDataStream && ( + + + + )} + {/* Since data stream and data retention are settings that are only allowed for non legacy, we only need to check if data stream is set to true to show the data retention. diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx index 9cb5c481b6b50..593655da62fef 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx @@ -22,7 +22,7 @@ import { EuiCodeBlock, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { getIndexModeLabel } from '../../../lib/index_mode_labels'; +import { indexModeLabels } from '../../../lib/index_mode_labels'; import { allowAutoCreateRadioIds } from '../../../../../common/constants'; import { serializers } from '../../../../shared_imports'; @@ -89,6 +89,7 @@ export const StepReview: React.FunctionComponent = React.memo( const { name, indexPatterns, + indexMode, version, order, template: indexTemplate, @@ -277,9 +278,7 @@ export const StepReview: React.FunctionComponent = React.memo( /> - {getIndexModeLabel( - serializedSettings?.['index.mode'] ?? serializedSettings?.index?.mode - )} + {indexModeLabels[indexMode]} {/* Mappings */} diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 2da3eef609a65..53b53a6ebdeee 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -11,7 +11,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { EuiSpacer, EuiButton, EuiPageHeader } from '@elastic/eui'; import { ScopedHistory } from '@kbn/core/public'; -import { allowAutoCreateRadioIds } from '../../../../common/constants'; +import { allowAutoCreateRadioIds, STANDARD_INDEX_MODE } from '../../../../common/constants'; import { TemplateDeserialized } from '../../../../common'; import { serializers, Forms, GlobalFlyout } from '../../../shared_imports'; import { @@ -118,6 +118,7 @@ export const TemplateForm = ({ name: '', indexPatterns: [], dataStream: {}, + indexMode: STANDARD_INDEX_MODE, template: {}, _kbnMeta: { type: 'default', @@ -341,7 +342,10 @@ export const TemplateForm = ({ )} - + diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx index 5448c932d65e0..2695da7adb813 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx @@ -23,6 +23,7 @@ import { allowAutoCreateRadioIds, INVALID_INDEX_PATTERN_CHARS, INVALID_TEMPLATE_NAME_CHARS, + STANDARD_INDEX_MODE, } from '../../../../common/constants'; const { @@ -150,6 +151,13 @@ export const schemas: Record = { }), defaultValue: false, }, + indexMode: { + type: FIELD_TYPES.SUPER_SELECT, + defaultValue: STANDARD_INDEX_MODE, + label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.fieldIndexModeLabel', { + defaultMessage: 'Index mode', + }), + }, order: { type: FIELD_TYPES.NUMBER, label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.fieldOrderLabel', { diff --git a/x-pack/plugins/index_management/public/application/lib/index_mode_labels.ts b/x-pack/plugins/index_management/public/application/lib/index_mode_labels.ts index 409659b8133c3..dd7a712567e9f 100644 --- a/x-pack/plugins/index_management/public/application/lib/index_mode_labels.ts +++ b/x-pack/plugins/index_management/public/application/lib/index_mode_labels.ts @@ -6,24 +6,43 @@ */ import { i18n } from '@kbn/i18n'; +import { + STANDARD_INDEX_MODE, + LOGSDB_INDEX_MODE, + TIME_SERIES_MODE, +} from '../../../common/constants'; -export const getIndexModeLabel = (mode?: string | null) => { - switch (mode) { - case 'standard': - case null: - case undefined: - return i18n.translate('xpack.idxMgmt.indexModeLabels.standardModeLabel', { - defaultMessage: 'Standard', - }); - case 'logsdb': - return i18n.translate('xpack.idxMgmt.indexModeLabels.logsdbModeLabel', { - defaultMessage: 'LogsDB', - }); - case 'time_series': - return i18n.translate('xpack.idxMgmt.indexModeLabels.tsdbModeLabel', { - defaultMessage: 'Time series', - }); - default: - return mode; - } +export const indexModeLabels = { + [STANDARD_INDEX_MODE]: i18n.translate('xpack.idxMgmt.indexModeLabels.standardIndexModeLabel', { + defaultMessage: 'Standard', + }), + [LOGSDB_INDEX_MODE]: i18n.translate('xpack.idxMgmt.indexModeLabels.logsdbIndexModeLabel', { + defaultMessage: 'LogsDB', + }), + [TIME_SERIES_MODE]: i18n.translate('xpack.idxMgmt.indexModeLabels.timeSeriesIndexModeLabel', { + defaultMessage: 'Time series', + }), +}; + +export const indexModeDescriptions = { + [STANDARD_INDEX_MODE]: i18n.translate( + 'xpack.idxMgmt.indexModeDescriptions.standardIndexModeDescription', + { + defaultMessage: + 'Standard indexing with default settings, for data other than logs or metrics', + } + ), + [LOGSDB_INDEX_MODE]: i18n.translate( + 'xpack.idxMgmt.indexModeDescriptions.logsdbIndexModeDescription', + { + defaultMessage: + 'Optimized for storing logs data, with reduced storage and default settings that help reduce the chance of logs being rejected by Elasticsearch', + } + ), + [TIME_SERIES_MODE]: i18n.translate( + 'xpack.idxMgmt.indexModeDescriptions.timeSeriesIndexModeDescription', + { + defaultMessage: 'Optimized for metrics data with reduced storage', + } + ), }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx index 5b3bf0920c3b7..d962305a7147c 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx @@ -34,7 +34,7 @@ import { EuiSpacer, } from '@elastic/eui'; -import { getIndexModeLabel } from '../../../../lib/index_mode_labels'; +import { indexModeLabels } from '../../../../lib/index_mode_labels'; import { DiscoverLink } from '../../../../lib/discover_link'; import { getLifecycleValue } from '../../../../lib/data_streams'; import { SectionLoading, reactRouterNavigate } from '../../../../../shared_imports'; @@ -355,7 +355,7 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ defaultMessage: "The index mode applied to the data stream's backing indices, as defined in its associated index template.", }), - content: getIndexModeLabel(indexMode), + content: indexModeLabels[indexMode], dataTestSubj: 'indexModeDetail', }, { diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx index e91fd644f795c..59daae719bf47 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx @@ -36,7 +36,7 @@ import { humanizeTimeStamp } from '../humanize_time_stamp'; import { DataStreamsBadges } from '../data_stream_badges'; import { ConditionalWrap } from '../data_stream_detail_panel'; import { isDataStreamFullyManagedByILM } from '../../../../lib/data_streams'; -import { getIndexModeLabel } from '../../../../lib/index_mode_labels'; +import { indexModeLabels } from '../../../../lib/index_mode_labels'; import { FilterListButton, Filters } from '../../components'; import { type DataStreamFilterName } from '../data_stream_list'; @@ -192,7 +192,7 @@ export const DataStreamTable: React.FunctionComponent = ({ }), truncateText: true, sortable: true, - render: (indexMode: DataStream['indexMode']) => getIndexModeLabel(indexMode), + render: (indexMode: DataStream['indexMode']) => indexModeLabels[indexMode], }); columns.push({ diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx index ff06a08014f61..2621f3ec483c1 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx @@ -28,7 +28,7 @@ import { TemplateDeserialized } from '../../../../../../../common'; import { ILM_PAGES_POLICY_EDIT } from '../../../../../constants'; import { useIlmLocator } from '../../../../../services/use_ilm_locator'; import { allowAutoCreateRadioIds } from '../../../../../../../common/constants'; -import { getIndexModeLabel } from '../../../../../lib/index_mode_labels'; +import { indexModeLabels } from '../../../../../lib/index_mode_labels'; interface Props { templateDetails: TemplateDeserialized; @@ -54,11 +54,11 @@ export const TabSummary: React.FunctionComponent = ({ templateDetails }) composedOf, order, indexPatterns = [], + indexMode, ilmPolicy, _meta, _kbnMeta: { isLegacy, hasDatastream }, allowAutoCreate, - template, } = templateDetails; const numIndexPatterns = indexPatterns.length; @@ -231,7 +231,7 @@ export const TabSummary: React.FunctionComponent = ({ templateDetails }) /> - {getIndexModeLabel(template?.settings?.index?.mode)} + {indexModeLabels[indexMode]} {/* Allow auto create */} diff --git a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts index 97cb0a1c1c69c..cfd9c1d18e610 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts @@ -13,6 +13,7 @@ export const templateSchema = schema.object({ version: schema.maybe(schema.number()), order: schema.maybe(schema.number()), priority: schema.maybe(schema.number()), + indexMode: schema.string(), // Not present for legacy templates allowAutoCreate: schema.maybe(schema.string()), template: schema.maybe( diff --git a/x-pack/plugins/index_management/test/fixtures/template.ts b/x-pack/plugins/index_management/test/fixtures/template.ts index 54df4410352b6..09895b550dc18 100644 --- a/x-pack/plugins/index_management/test/fixtures/template.ts +++ b/x-pack/plugins/index_management/test/fixtures/template.ts @@ -23,6 +23,7 @@ export const getComposableTemplate = ({ isLegacy = false, type = 'default', allowAutoCreate = 'NO_OVERWRITE', + indexMode = 'standard', composedOf = [], }: Partial< TemplateDeserialized & { @@ -42,6 +43,7 @@ export const getComposableTemplate = ({ version, priority, indexPatterns, + indexMode, allowAutoCreate, template: { aliases, @@ -66,6 +68,7 @@ export const getTemplate = ({ order = getRandomNumber(), indexPatterns = [], template: { settings, aliases, mappings } = {}, + indexMode = 'standard', dataStream, composedOf, ignoreMissingComponentTemplates, @@ -85,6 +88,7 @@ export const getTemplate = ({ version, order, indexPatterns, + indexMode, allowAutoCreate, template: { aliases, diff --git a/x-pack/plugins/kubernetes_security/server/routes/aggregate.ts b/x-pack/plugins/kubernetes_security/server/routes/aggregate.ts index f83ddc818cbb4..4ddb828b68976 100644 --- a/x-pack/plugins/kubernetes_security/server/routes/aggregate.ts +++ b/x-pack/plugins/kubernetes_security/server/routes/aggregate.ts @@ -38,6 +38,11 @@ export const registerAggregateRoute = (router: IRouter, logger: Logger) => { .addVersion( { version: '1', + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, validate: { request: { query: schema.object({ diff --git a/x-pack/plugins/kubernetes_security/server/routes/count.ts b/x-pack/plugins/kubernetes_security/server/routes/count.ts index 0922adeb0cf45..b73452e8e45fc 100644 --- a/x-pack/plugins/kubernetes_security/server/routes/count.ts +++ b/x-pack/plugins/kubernetes_security/server/routes/count.ts @@ -28,6 +28,11 @@ export const registerCountRoute = (router: IRouter, logger: Logger) => { .addVersion( { version: '1', + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, validate: { request: { query: schema.object({ diff --git a/x-pack/plugins/kubernetes_security/server/routes/multi_terms_aggregate.ts b/x-pack/plugins/kubernetes_security/server/routes/multi_terms_aggregate.ts index 83f5b70efe051..b4a0271b63edc 100644 --- a/x-pack/plugins/kubernetes_security/server/routes/multi_terms_aggregate.ts +++ b/x-pack/plugins/kubernetes_security/server/routes/multi_terms_aggregate.ts @@ -35,6 +35,11 @@ export const registerMultiTermsAggregateRoute = (router: IRouter, logger: Logger .addVersion( { version: '1', + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, validate: { request: { query: schema.object({ diff --git a/x-pack/plugins/monitoring/kibana.jsonc b/x-pack/plugins/monitoring/kibana.jsonc index 51272f995b012..5b9b2853357b9 100644 --- a/x-pack/plugins/monitoring/kibana.jsonc +++ b/x-pack/plugins/monitoring/kibana.jsonc @@ -21,7 +21,8 @@ "observability", "observabilityShared", "dataViews", - "unifiedSearch" + "unifiedSearch", + "share" ], "optionalPlugins": [ "infra", @@ -39,7 +40,8 @@ "requiredBundles": [ "kibanaUtils", "alerting", - "kibanaReact" + "kibanaReact", + "logsShared" ] } } \ No newline at end of file diff --git a/x-pack/plugins/monitoring/public/components/logs/logs.js b/x-pack/plugins/monitoring/public/components/logs/logs.js index 3678c4ae9e27f..3da4bc9dc13ff 100644 --- a/x-pack/plugins/monitoring/public/components/logs/logs.js +++ b/x-pack/plugins/monitoring/public/components/logs/logs.js @@ -10,11 +10,12 @@ import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; import { upperFirst } from 'lodash'; import { Legacy } from '../../legacy_shims'; import { EuiBasicTable, EuiTitle, EuiSpacer, EuiText, EuiCallOut, EuiLink } from '@elastic/eui'; -import { INFRA_SOURCE_ID } from '../../../common/constants'; import { formatDateTimeLocal } from '../../../common/formatting'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { Reason } from './reason'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { getLogsLocatorsFromUrlService } from '@kbn/logs-shared-plugin/common'; const getFormattedDateTimeLocal = (timestamp) => { const timezone = Legacy.shims.uiSettings?.get('dateFormat:tz'); @@ -110,7 +111,7 @@ const clusterColumns = [ }, ]; -function getLogsUiLink(clusterUuid, nodeId, indexUuid) { +function getLogsUiLink(clusterUuid, nodeId, indexUuid, sharePlugin) { const params = []; if (clusterUuid) { params.push(`elasticsearch.cluster.uuid:${clusterUuid}`); @@ -122,15 +123,23 @@ function getLogsUiLink(clusterUuid, nodeId, indexUuid) { params.push(`elasticsearch.index.name:${indexUuid}`); } - const base = Legacy.shims.infra.locators.logsLocator.getRedirectUrl({ - logView: { logViewId: INFRA_SOURCE_ID, type: 'log-view-reference' }, - ...(params.length ? { filter: params.join(' and ') } : {}), + const filter = params.join(' and '); + const { logsLocator } = getLogsLocatorsFromUrlService(sharePlugin.url); + + const base = logsLocator.getRedirectUrl({ + filter, }); return base; } -export class Logs extends PureComponent { +export const Logs = (props) => { + const { + services: { share }, + } = useKibana(); + return ; +}; +export class LogsContent extends PureComponent { renderLogs() { const { logs: { enabled, logs }, @@ -165,7 +174,9 @@ export class Logs extends PureComponent { nodeId, clusterUuid, indexUuid, + sharePlugin, } = this.props; + if (!enabled || !show) { return null; } @@ -184,7 +195,7 @@ export class Logs extends PureComponent { defaultMessage="Visit {link} to dive deeper." values={{ link: ( - + {i18n.translate('xpack.monitoring.logs.listing.calloutLinkText', { defaultMessage: 'Logs', })} diff --git a/x-pack/plugins/monitoring/public/components/logs/logs.test.js b/x-pack/plugins/monitoring/public/components/logs/logs.test.js index 23292ff742fcd..1117a484a294c 100644 --- a/x-pack/plugins/monitoring/public/components/logs/logs.test.js +++ b/x-pack/plugins/monitoring/public/components/logs/logs.test.js @@ -7,7 +7,18 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Logs } from './logs'; +import { LogsContent } from './logs'; +import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; + +const sharePlugin = sharePluginMock.createStartContract(); + +jest.mock('@kbn/logs-shared-plugin/common', () => { + return { + getLogsLocatorsFromUrlService: jest.fn().mockReturnValue({ + logsLocator: { getRedirectUrl: jest.fn(() => '') }, + }), + }; +}); jest.mock('../../legacy_shims', () => ({ Legacy: { @@ -123,32 +134,40 @@ const logs = { describe('Logs', () => { it('should render normally', () => { - const component = shallow(); + const component = shallow(); expect(component).toMatchSnapshot(); }); it('should render fewer columns for node or index view', () => { - const component = shallow(); + const component = shallow(); expect(component.find('EuiBasicTable').prop('columns')).toMatchSnapshot(); }); it('should render a link to filter by cluster uuid', () => { - const component = shallow(); + const component = shallow( + + ); expect(component.find('EuiCallOut')).toMatchSnapshot(); }); it('should render a link to filter by cluster uuid and node uuid', () => { - const component = shallow(); + const component = shallow( + + ); expect(component.find('EuiCallOut')).toMatchSnapshot(); }); it('should render a link to filter by cluster uuid and index uuid', () => { - const component = shallow(); + const component = shallow( + + ); expect(component.find('EuiCallOut')).toMatchSnapshot(); }); it('should render a reason if the logs are disabled', () => { - const component = shallow(); + const component = shallow( + + ); expect(component).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index c6cfbdc48094c..e29faa9ca5d86 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -101,6 +101,7 @@ export class MonitoringPlugin element: params.element, core: coreStart, data: pluginsStart.data, + share: pluginsStart.share, isCloud: Boolean(plugins.cloud?.isCloudEnabled), pluginInitializerContext: this.initializerContext, externalConfig, @@ -124,6 +125,7 @@ export class MonitoringPlugin appMountParameters: deps.appMountParameters, dataViews: deps.dataViews, infra: deps.infra, + share: deps.share, }); const config = Object.fromEntries(externalConfig); diff --git a/x-pack/plugins/monitoring/public/types.ts b/x-pack/plugins/monitoring/public/types.ts index 5e6590968d232..cd5314521f947 100644 --- a/x-pack/plugins/monitoring/public/types.ts +++ b/x-pack/plugins/monitoring/public/types.ts @@ -16,6 +16,7 @@ import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { DashboardStart } from '@kbn/dashboard-plugin/public'; import { FleetStart } from '@kbn/fleet-plugin/public'; import type { InfraClientStartExports } from '@kbn/infra-plugin/public'; +import { SharePluginStart } from '@kbn/share-plugin/public'; export interface MonitoringStartPluginDependencies { navigation: NavigationStart; @@ -26,6 +27,7 @@ export interface MonitoringStartPluginDependencies { dashboard?: DashboardStart; fleet?: FleetStart; infra?: InfraClientStartExports; + share: SharePluginStart; } interface LegacyStartDependencies { diff --git a/x-pack/plugins/monitoring/tsconfig.json b/x-pack/plugins/monitoring/tsconfig.json index 3b78104e65b8c..48b538a00c1eb 100644 --- a/x-pack/plugins/monitoring/tsconfig.json +++ b/x-pack/plugins/monitoring/tsconfig.json @@ -48,6 +48,7 @@ "@kbn/flot-charts", "@kbn/ui-theme", "@kbn/core-elasticsearch-server", + "@kbn/share-plugin", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/config.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/config.ts index 4df8891bd06fc..2f36d27889c14 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/config.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/config.ts @@ -9,7 +9,6 @@ import { schema, type TypeOf } from '@kbn/config-schema'; export const config = schema.object({ enabled: schema.boolean({ defaultValue: true }), - modelId: schema.maybe(schema.string()), // TODO: Remove scope: schema.maybe(schema.oneOf([schema.literal('observability'), schema.literal('search')])), enableKnowledgeBase: schema.boolean({ defaultValue: true }), }); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts index 98a6232563054..b2b5736fd1d6f 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts @@ -116,11 +116,6 @@ export class ObservabilityAIAssistantPlugin // Using once to make sure the same model ID is used during service init and Knowledge base setup const getSearchConnectorModelId = once(async () => { - // TODO: Remove this once the modelId is removed from the config - const configModelId = this.config.modelId; - if (configModelId) { - return configModelId; - } const defaultModelId = '.elser_model_2'; const [_, pluginsStart] = await core.getStartServices(); // Wait for the license to be available so the ML plugin's guards pass once we ask for ELSER stats diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/inference_endpoint.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/inference_endpoint.ts index 1d09311dbd6ea..e89028652d9ac 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/inference_endpoint.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/inference_endpoint.ts @@ -10,7 +10,7 @@ import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { Logger } from '@kbn/logging'; import moment from 'moment'; -export const AI_ASSISTANT_KB_INFERENCE_ID = 'ai_assistant_kb_inference'; +export const AI_ASSISTANT_KB_INFERENCE_ID = 'obs_ai_assistant_kb_inference'; export async function createInferenceEndpoint({ esClient, diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/bulk_cruds/edit_monitor_bulk.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/bulk_cruds/edit_monitor_bulk.ts index 833e0203e3817..f106dc46c0708 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/bulk_cruds/edit_monitor_bulk.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/bulk_cruds/edit_monitor_bulk.ts @@ -28,7 +28,6 @@ import { export interface MonitorConfigUpdate { normalizedMonitor: SyntheticsMonitor; monitorWithRevision: SyntheticsMonitorWithSecretsAttributes; - previousMonitor: SavedObject; decryptedPreviousMonitor: SavedObject; } @@ -40,14 +39,14 @@ const updateConfigSavedObjects = async ({ monitorsToUpdate: MonitorConfigUpdate[]; }) => { return await routeContext.savedObjectsClient.bulkUpdate( - monitorsToUpdate.map(({ previousMonitor, monitorWithRevision }) => ({ + monitorsToUpdate.map(({ monitorWithRevision, decryptedPreviousMonitor }) => ({ type: syntheticsMonitorType, - id: previousMonitor.id, + id: decryptedPreviousMonitor.id, attributes: { ...monitorWithRevision, - [ConfigKey.CONFIG_ID]: previousMonitor.id, + [ConfigKey.CONFIG_ID]: decryptedPreviousMonitor.id, [ConfigKey.MONITOR_QUERY_ID]: - monitorWithRevision[ConfigKey.CUSTOM_HEARTBEAT_ID] || previousMonitor.id, + monitorWithRevision[ConfigKey.CUSTOM_HEARTBEAT_ID] || decryptedPreviousMonitor.id, }, })) ); @@ -67,15 +66,14 @@ async function syncUpdatedMonitors({ const { syntheticsMonitorClient } = routeContext; return await syntheticsMonitorClient.editMonitors( - monitorsToUpdate.map(({ normalizedMonitor, previousMonitor, decryptedPreviousMonitor }) => ({ + monitorsToUpdate.map(({ normalizedMonitor, decryptedPreviousMonitor }) => ({ monitor: { ...(normalizedMonitor as MonitorFields), - [ConfigKey.CONFIG_ID]: previousMonitor.id, + [ConfigKey.CONFIG_ID]: decryptedPreviousMonitor.id, [ConfigKey.MONITOR_QUERY_ID]: - normalizedMonitor[ConfigKey.CUSTOM_HEARTBEAT_ID] || previousMonitor.id, + normalizedMonitor[ConfigKey.CUSTOM_HEARTBEAT_ID] || decryptedPreviousMonitor.id, }, - id: previousMonitor.id, - previousMonitor, + id: decryptedPreviousMonitor.id, decryptedPreviousMonitor, })), privateLocations, @@ -104,9 +102,9 @@ export const syncEditedMonitorBulk = async ({ const { failedPolicyUpdates, publicSyncErrors } = editSyncResponse; - monitorsToUpdate.forEach(({ normalizedMonitor, previousMonitor }) => { + monitorsToUpdate.forEach(({ normalizedMonitor, decryptedPreviousMonitor }) => { const editedMonitorSavedObject = editedMonitorSavedObjects?.saved_objects.find( - (obj) => obj.id === previousMonitor.id + (obj) => obj.id === decryptedPreviousMonitor.id ); sendTelemetryEvents( @@ -114,7 +112,7 @@ export const syncEditedMonitorBulk = async ({ server.telemetry, formatTelemetryUpdateEvent( editedMonitorSavedObject as SavedObjectsUpdateResponse, - previousMonitor, + decryptedPreviousMonitor.updated_at, server.stackVersion, Boolean((normalizedMonitor as MonitorFields)[ConfigKey.SOURCE_INLINE]), publicSyncErrors @@ -150,9 +148,9 @@ export const rollbackCompletely = async ({ const { savedObjectsClient, server } = routeContext; try { await savedObjectsClient.bulkUpdate( - monitorsToUpdate.map(({ previousMonitor, decryptedPreviousMonitor }) => ({ + monitorsToUpdate.map(({ decryptedPreviousMonitor }) => ({ type: syntheticsMonitorType, - id: previousMonitor.id, + id: decryptedPreviousMonitor.id, attributes: decryptedPreviousMonitor.attributes, })) ); @@ -167,7 +165,6 @@ export const rollbackFailedUpdates = async ({ monitorsToUpdate, }: { monitorsToUpdate: Array<{ - previousMonitor: SavedObject; decryptedPreviousMonitor: SavedObject; }>; routeContext: RouteContext; @@ -194,12 +191,12 @@ export const rollbackFailedUpdates = async ({ }); const monitorsToRevert = monitorsToUpdate - .filter(({ previousMonitor }) => { - return failedConfigs[previousMonitor.id]; + .filter(({ decryptedPreviousMonitor }) => { + return failedConfigs[decryptedPreviousMonitor.id]; }) - .map(({ previousMonitor, decryptedPreviousMonitor }) => ({ + .map(({ decryptedPreviousMonitor }) => ({ type: syntheticsMonitorType, - id: previousMonitor.id, + id: decryptedPreviousMonitor.id, attributes: decryptedPreviousMonitor.attributes, })); diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/edit_monitor.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/edit_monitor.ts index d9261847db1ab..d460b71037950 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/edit_monitor.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/edit_monitor.ts @@ -272,7 +272,7 @@ export const syncEditedMonitor = async ({ server.telemetry, formatTelemetryUpdateEvent( editedMonitorSavedObject as SavedObjectsUpdateResponse, - decryptedPreviousMonitor, + decryptedPreviousMonitor.updated_at, server.stackVersion, Boolean((normalizedMonitor as MonitorFields)[ConfigKey.SOURCE_INLINE]), publicSyncErrors diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/get_monitor_project.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/get_monitor_project.ts index b0b4eee392d56..ac55807f9412f 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/get_monitor_project.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/get_monitor_project.ts @@ -32,7 +32,7 @@ export const getSyntheticsProjectMonitorsRoute: SyntheticsRestApiRouteFactory = } = routeContext; const { projectName } = request.params; - const { per_page: perPage = 500, search_after: searchAfter } = request.query; + const { per_page: perPage = 1000, search_after: searchAfter } = request.query; const decodedProjectName = decodeURI(projectName); const decodedSearchAfter = searchAfter ? decodeURI(searchAfter) : undefined; diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/telemetry/monitor_upgrade_sender.test.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/telemetry/monitor_upgrade_sender.test.ts index 299ab62f09028..761472bb7fa8c 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/telemetry/monitor_upgrade_sender.test.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/telemetry/monitor_upgrade_sender.test.ts @@ -152,7 +152,7 @@ describe('monitor upgrade telemetry helpers', () => { it('handles formatting update events', () => { const actual = formatTelemetryUpdateEvent( createTestConfig({}, '2011-10-05T16:48:00.000Z'), - testConfig, + testConfig.updated_at, stackVersion, false, errors diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/telemetry/monitor_upgrade_sender.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/telemetry/monitor_upgrade_sender.ts index f008d00a18536..f000108620f45 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/telemetry/monitor_upgrade_sender.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/telemetry/monitor_upgrade_sender.ts @@ -109,23 +109,22 @@ export function formatTelemetryEvent({ export function formatTelemetryUpdateEvent( currentMonitor: SavedObjectsUpdateResponse, - previousMonitor: SavedObject, + previousMonitorUpdatedAt: string | undefined, stackVersion: string, isInlineScript: boolean, errors?: ServiceLocationErrors | null ) { let durationSinceLastUpdated: number = 0; - if (currentMonitor.updated_at && previousMonitor.updated_at) { + if (currentMonitor.updated_at && previousMonitorUpdatedAt) { durationSinceLastUpdated = - new Date(currentMonitor.updated_at).getTime() - - new Date(previousMonitor.updated_at).getTime(); + new Date(currentMonitor.updated_at).getTime() - new Date(previousMonitorUpdatedAt).getTime(); } return formatTelemetryEvent({ stackVersion, monitor: currentMonitor as SavedObject, durationSinceLastUpdated, - lastUpdatedAt: previousMonitor.updated_at, + lastUpdatedAt: previousMonitorUpdatedAt, isInlineScript, errors, }); diff --git a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/project_monitor_formatter.ts b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/project_monitor_formatter.ts index 6069076e375a2..1fe5d30048418 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/project_monitor_formatter.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/project_monitor_formatter.ts @@ -45,6 +45,17 @@ import { normalizeProjectMonitor } from './normalizers'; type FailedError = Array<{ id?: string; reason: string; details: string; payload?: object }>; +export interface ExistingMonitor { + [ConfigKey.JOURNEY_ID]: string; + [ConfigKey.CONFIG_ID]: string; + [ConfigKey.REVISION]: number; + [ConfigKey.MONITOR_TYPE]: string; +} + +export interface PreviousMonitorForUpdate extends ExistingMonitor { + updated_at?: string; +} + export const CANNOT_UPDATE_MONITOR_TO_DIFFERENT_TYPE = i18n.translate( 'xpack.synthetics.service.projectMonitors.cannotUpdateMonitorToDifferentType', { @@ -128,14 +139,13 @@ export class ProjectMonitorFormatter { const normalizedNewMonitors: SyntheticsMonitor[] = []; const normalizedUpdateMonitors: Array<{ - previousMonitor: SavedObjectsFindResult; + previousMonitor: PreviousMonitorForUpdate; monitor: SyntheticsMonitor; }> = []; for (const monitor of this.monitors) { const previousMonitor = existingMonitors.find( - (monitorObj) => - (monitorObj.attributes as SyntheticsMonitor)[ConfigKey.JOURNEY_ID] === monitor.id + (monitorObj) => monitorObj[ConfigKey.JOURNEY_ID] === monitor.id ); const normM = await this.validateProjectMonitor({ @@ -146,7 +156,7 @@ export class ProjectMonitorFormatter { if (normM) { if ( previousMonitor && - previousMonitor.attributes[ConfigKey.MONITOR_TYPE] !== normM[ConfigKey.MONITOR_TYPE] + previousMonitor[ConfigKey.MONITOR_TYPE] !== normM[ConfigKey.MONITOR_TYPE] ) { this.failedMonitors.push({ reason: CANNOT_UPDATE_MONITOR_TO_DIFFERENT_TYPE, @@ -157,7 +167,7 @@ export class ProjectMonitorFormatter { 'Monitor {monitorId} of type {previousType} cannot be updated to type {currentType}. Please delete the monitor first and try again.', values: { currentType: monitor.type, - previousType: previousMonitor.attributes[ConfigKey.MONITOR_TYPE], + previousType: previousMonitor[ConfigKey.MONITOR_TYPE], monitorId: monitor.id, }, } @@ -246,19 +256,33 @@ export class ProjectMonitorFormatter { } }; - public getProjectMonitorsForProject = async () => { - const finder = this.savedObjectsClient.createPointInTimeFinder({ + public getProjectMonitorsForProject = async (): Promise => { + const journeyIds = this.monitors.map((monitor) => monitor.id); + const journeyFilter = getSavedObjectKqlFilter({ + field: ConfigKey.JOURNEY_ID, + values: journeyIds, + }); + const finder = this.savedObjectsClient.createPointInTimeFinder({ type: syntheticsMonitorType, perPage: 5000, - filter: this.projectFilter, + filter: `${this.projectFilter} AND ${journeyFilter}`, + fields: [ + ConfigKey.JOURNEY_ID, + ConfigKey.CONFIG_ID, + ConfigKey.REVISION, + ConfigKey.MONITOR_TYPE, + ], }); - const hits: Array> = []; + const hits: PreviousMonitorForUpdate[] = []; for await (const result of finder.find()) { hits.push( - ...(result.saved_objects as Array< - SavedObjectsFindResult - >) + ...result.saved_objects.map((monitor) => { + return { + ...monitor.attributes, + updated_at: monitor.updated_at, + }; + }) ); } @@ -333,10 +357,8 @@ export class ProjectMonitorFormatter { } }; - private getDecryptedMonitors = async ( - monitors: Array> - ) => { - const configIds = monitors.map((monitor) => monitor.attributes[ConfigKey.CONFIG_ID]); + private getDecryptedMonitors = async (monitors: PreviousMonitorForUpdate[]) => { + const configIds = monitors.map((monitor) => monitor[ConfigKey.CONFIG_ID]); const monitorFilter = getSavedObjectKqlFilter({ field: ConfigKey.CONFIG_ID, values: configIds, @@ -344,7 +366,7 @@ export class ProjectMonitorFormatter { const finder = await this.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser( { - search: monitorFilter, + filter: monitorFilter, type: syntheticsMonitorType, perPage: 500, namespaces: [this.spaceId], @@ -365,7 +387,7 @@ export class ProjectMonitorFormatter { private updateMonitorsBulk = async ( monitors: Array<{ monitor: SyntheticsMonitor; - previousMonitor: SavedObjectsFindResult; + previousMonitor: PreviousMonitorForUpdate; }> ): Promise< | { @@ -390,10 +412,11 @@ export class ProjectMonitorFormatter { const monitorsToUpdate: MonitorConfigUpdate[] = []; decryptedPreviousMonitors.forEach((decryptedPreviousMonitor) => { - const monitor = monitors.find((m) => m.previousMonitor.id === decryptedPreviousMonitor.id); + const monitor = monitors.find( + (m) => m.previousMonitor[ConfigKey.CONFIG_ID] === decryptedPreviousMonitor.id + ); if (monitor) { const normalizedMonitor = monitor?.monitor; - const previousMonitor = monitor?.previousMonitor; const { attributes: { [ConfigKey.REVISION]: _, ...normalizedPrevMonitorAttr }, } = normalizeSecrets(decryptedPreviousMonitor); @@ -401,11 +424,10 @@ export class ProjectMonitorFormatter { const monitorWithRevision = formatSecrets({ ...normalizedPrevMonitorAttr, ...normalizedMonitor, - revision: (previousMonitor.attributes[ConfigKey.REVISION] || 0) + 1, + revision: (decryptedPreviousMonitor.attributes[ConfigKey.REVISION] || 0) + 1, }); monitorsToUpdate.push({ normalizedMonitor, - previousMonitor, monitorWithRevision, decryptedPreviousMonitor, }); diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.test.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.test.tsx index e0199ab40168d..fff9450b6a1cb 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.test.tsx @@ -11,6 +11,9 @@ import { AlertsPreview } from './alerts_preview'; import { TestProviders } from '../../../common/mock/test_providers'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import type { ParsedAlertsData } from '../../../overview/components/detection_response/alerts_by_status/types'; +import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; +import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; +import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score'; const mockAlertsData: ParsedAlertsData = { open: { @@ -29,9 +32,10 @@ const mockAlertsData: ParsedAlertsData = { }, }; -jest.mock( - '../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data' -); +// Mock hooks +jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'); +jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'); +jest.mock('../../../entity_analytics/api/hooks/use_risk_score'); jest.mock('@kbn/expandable-flyout'); describe('AlertsPreview', () => { @@ -39,6 +43,13 @@ describe('AlertsPreview', () => { beforeEach(() => { (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel }); + (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({ + data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } }, + }); + (useRiskScore as jest.Mock).mockReturnValue({ data: [{ host: { risk: 75 } }] }); + (useMisconfigurationPreview as jest.Mock).mockReturnValue({ + data: { count: { passed: 1, failed: 1 } }, + }); }); afterEach(() => { jest.clearAllMocks(); @@ -47,17 +58,17 @@ describe('AlertsPreview', () => { it('renders', () => { const { getByTestId } = render( - + ); - expect(getByTestId('securitySolutionFlyoutInsightsAlertsTitleText')).toBeInTheDocument(); + expect(getByTestId('securitySolutionFlyoutInsightsAlertsTitleLink')).toBeInTheDocument(); }); it('renders correct alerts number', () => { const { getByTestId } = render( - + ); @@ -67,7 +78,7 @@ describe('AlertsPreview', () => { it('should render the correct number of distribution bar section based on the number of severities', () => { const { queryAllByTestId } = render( - + ); diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx index 3f9a0115d9ed1..c832f12c93f78 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx @@ -5,19 +5,40 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { capitalize } from 'lodash'; import type { EuiThemeComputed } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle, useEuiTheme } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { DistributionBar } from '@kbn/security-solution-distribution-bar'; -import { getAbbreviatedNumber } from '@kbn/cloud-security-posture-common'; -import { ExpandablePanel } from '../../../flyout/shared/components/expandable_panel'; -import { getSeverityColor } from '../../../detections/components/alerts_kpis/severity_level_panel/helpers'; +import { + buildEntityFlyoutPreviewQuery, + getAbbreviatedNumber, +} from '@kbn/cloud-security-posture-common'; +import { hasVulnerabilitiesData } from '@kbn/cloud-security-posture'; +import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; +import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import type { AlertsByStatus, ParsedAlertsData, } from '../../../overview/components/detection_response/alerts_by_status/types'; +import { ExpandablePanel } from '../../../flyout/shared/components/expandable_panel'; +import { getSeverityColor } from '../../../detections/components/alerts_kpis/severity_level_panel/helpers'; +import type { HostRiskScore, UserRiskScore } from '../../../../common/search_strategy'; +import { + buildHostNamesFilter, + buildUserNamesFilter, + RiskScoreEntity, +} from '../../../../common/search_strategy'; +import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score'; +import { FIRST_RECORD_PAGINATION } from '../../../entity_analytics/common'; +import { HostDetailsPanelKey } from '../../../flyout/entity_details/host_details_left'; +import { + EntityDetailsLeftPanelTab, + CspInsightLeftPanelSubTab, +} from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; +import { UserDetailsPanelKey } from '../../../flyout/entity_details/user_details_left'; const AlertsCount = ({ alertsTotal, @@ -56,9 +77,13 @@ const AlertsCount = ({ export const AlertsPreview = ({ alertsData, + fieldName, + name, isPreviewMode, }: { alertsData: ParsedAlertsData; + fieldName: string; + name: string; isPreviewMode?: boolean; }) => { const { euiTheme } = useEuiTheme(); @@ -82,9 +107,120 @@ export const AlertsPreview = ({ const totalAlertsCount = alertStats.reduce((total, item) => total + item.count, 0); + const { data } = useMisconfigurationPreview({ + query: buildEntityFlyoutPreviewQuery(fieldName, name), + sort: [], + enabled: true, + pageSize: 1, + ignore_unavailable: true, + }); + const isUsingHostName = fieldName === 'host.name'; + const passedFindings = data?.count.passed || 0; + const failedFindings = data?.count.failed || 0; + + const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0; + + const { data: vulnerabilitiesData } = useVulnerabilitiesPreview({ + query: buildEntityFlyoutPreviewQuery('host.name', name), + sort: [], + enabled: true, + pageSize: 1, + }); + + const { + CRITICAL = 0, + HIGH = 0, + MEDIUM = 0, + LOW = 0, + NONE = 0, + } = vulnerabilitiesData?.count || {}; + + const hasVulnerabilitiesFindings = hasVulnerabilitiesData({ + critical: CRITICAL, + high: HIGH, + medium: MEDIUM, + low: LOW, + none: NONE, + }); + + const buildFilterQuery = useMemo( + () => (isUsingHostName ? buildHostNamesFilter([name]) : buildUserNamesFilter([name])), + [isUsingHostName, name] + ); + + const riskScoreState = useRiskScore({ + riskEntity: isUsingHostName ? RiskScoreEntity.host : RiskScoreEntity.user, + filterQuery: buildFilterQuery, + onlyLatest: false, + pagination: FIRST_RECORD_PAGINATION, + }); + + const { data: hostRisk } = riskScoreState; + + const riskData = hostRisk?.[0]; + + const isRiskScoreExist = isUsingHostName + ? !!(riskData as HostRiskScore)?.host.risk + : !!(riskData as UserRiskScore)?.user.risk; + + const hasNonClosedAlerts = totalAlertsCount > 0; + + const { openLeftPanel } = useExpandableFlyoutApi(); + + const goToEntityInsightTab = useCallback(() => { + openLeftPanel({ + id: isUsingHostName ? HostDetailsPanelKey : UserDetailsPanelKey, + params: isUsingHostName + ? { + name, + isRiskScoreExist, + hasMisconfigurationFindings, + hasVulnerabilitiesFindings, + hasNonClosedAlerts, + path: { + tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS, + subTab: CspInsightLeftPanelSubTab.ALERTS, + }, + } + : { + user: { name }, + isRiskScoreExist, + hasMisconfigurationFindings, + hasNonClosedAlerts, + path: { + tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS, + subTab: CspInsightLeftPanelSubTab.ALERTS, + }, + }, + }); + }, [ + hasMisconfigurationFindings, + hasNonClosedAlerts, + hasVulnerabilitiesFindings, + isRiskScoreExist, + isUsingHostName, + name, + openLeftPanel, + ]); + const link = useMemo( + () => + !isPreviewMode + ? { + callback: goToEntityInsightTab, + tooltip: ( + + ), + } + : undefined, + [isPreviewMode, goToEntityInsightTab] + ); return ( ), + link: totalAlertsCount > 0 ? link : undefined, }} data-test-subj={'securitySolutionFlyoutInsightsAlerts'} > diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/alerts_findings_details_table.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/alerts_findings_details_table.tsx new file mode 100644 index 0000000000000..966de68e3497f --- /dev/null +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/alerts_findings_details_table.tsx @@ -0,0 +1,265 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useCallback, useEffect, useState } from 'react'; +import { capitalize } from 'lodash'; +import type { Criteria, EuiBasicTableColumn } from '@elastic/eui'; +import { EuiSpacer, EuiPanel, EuiText, EuiBasicTable, EuiIcon, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { DistributionBar } from '@kbn/security-solution-distribution-bar'; +import { + ENTITY_FLYOUT_EXPAND_MISCONFIGURATION_VIEW_VISITS, + uiMetricService, +} from '@kbn/cloud-security-posture-common/utils/ui_metrics'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { buildEntityAlertsQuery } from '@kbn/cloud-security-posture-common/utils/helpers'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { TableId } from '@kbn/securitysolution-data-table'; +import { + OPEN_IN_ALERTS_TITLE_HOSTNAME, + OPEN_IN_ALERTS_TITLE_STATUS, + OPEN_IN_ALERTS_TITLE_USERNAME, +} from '../../../overview/components/detection_response/translations'; +import { useNavigateToAlertsPageWithFilters } from '../../../common/hooks/use_navigate_to_alerts_page_with_filters'; +import { DocumentDetailsPreviewPanelKey } from '../../../flyout/document_details/shared/constants/panel_keys'; +import { useGlobalTime } from '../../../common/containers/use_global_time'; +import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query'; +import { ALERTS_QUERY_NAMES } from '../../../detections/containers/detection_engine/alerts/constants'; +import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index'; +import { getSeverityColor } from '../../../detections/components/alerts_kpis/severity_level_panel/helpers'; +import { SeverityBadge } from '../../../common/components/severity_badge'; +import { ALERT_PREVIEW_BANNER } from '../../../flyout/document_details/preview/constants'; +import { FILTER_OPEN, FILTER_ACKNOWLEDGED } from '../../../../common/types'; + +type AlertSeverity = 'low' | 'medium' | 'high' | 'critical'; + +interface ResultAlertsField { + _id: string[]; + _index: string[]; + 'kibana.alert.rule.uuid': string[]; + 'kibana.alert.severity': AlertSeverity[]; + 'kibana.alert.rule.name': string[]; + 'kibana.alert.workflow_status': string[]; +} + +interface ContextualFlyoutAlertsField { + id: string; + index: string; + ruleUuid: string; + ruleName: string; + severity: AlertSeverity; + status: string; +} + +interface AlertsDetailsFields { + fields: ResultAlertsField; +} + +export const AlertsDetailsTable = memo( + ({ fieldName, queryName }: { fieldName: 'host.name' | 'user.name'; queryName: string }) => { + useEffect(() => { + uiMetricService.trackUiMetric( + METRIC_TYPE.COUNT, + ENTITY_FLYOUT_EXPAND_MISCONFIGURATION_VIEW_VISITS + ); + }, []); + + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(10); + + const alertsPagination = (alerts: ContextualFlyoutAlertsField[]) => { + let pageOfItems; + + if (!pageIndex && !pageSize) { + pageOfItems = alerts; + } else { + const startIndex = pageIndex * pageSize; + pageOfItems = alerts?.slice(startIndex, Math.min(startIndex + pageSize, alerts?.length)); + } + + return { + pageOfItems, + totalItemCount: alerts?.length, + }; + }; + + const { to, from } = useGlobalTime(); + const { signalIndexName } = useSignalIndex(); + const { data } = useQueryAlerts({ + query: buildEntityAlertsQuery(fieldName, to, from, queryName, 500), + queryName: ALERTS_QUERY_NAMES.BY_RULE_BY_STATUS, + indexName: signalIndexName, + }); + + const alertDataResults = (data?.hits?.hits as AlertsDetailsFields[])?.map( + (item: AlertsDetailsFields) => { + return { + id: item.fields?._id?.[0], + index: item.fields?._index?.[0], + ruleName: item.fields?.['kibana.alert.rule.name']?.[0], + ruleUuid: item.fields?.['kibana.alert.rule.uuid']?.[0], + severity: item.fields?.['kibana.alert.severity']?.[0], + status: item.fields?.['kibana.alert.workflow_status']?.[0], + }; + } + ); + + const severitiesMap = alertDataResults?.map((item) => item.severity) || []; + + const alertStats = Object.entries( + severitiesMap.reduce((acc: Record, item) => { + acc[item] = (acc[item] || 0) + 1; + return acc; + }, {}) + ).map(([key, count]) => ({ + key: capitalize(key), + count, + color: getSeverityColor(key), + })); + + const { pageOfItems, totalItemCount } = alertsPagination(alertDataResults || []); + + const pagination = { + pageIndex, + pageSize, + totalItemCount, + pageSizeOptions: [10, 25, 100], + }; + + const onTableChange = ({ page }: Criteria) => { + if (page) { + const { index, size } = page; + setPageIndex(index); + setPageSize(size); + } + }; + + const { openPreviewPanel } = useExpandableFlyoutApi(); + + const handleOnEventAlertDetailPanelOpened = useCallback( + (eventId: string, indexName: string, tableId: string) => { + openPreviewPanel({ + id: DocumentDetailsPreviewPanelKey, + params: { + id: eventId, + indexName, + scopeId: tableId, + isPreviewMode: true, + banner: ALERT_PREVIEW_BANNER, + }, + }); + }, + [openPreviewPanel] + ); + + const tableId = TableId.alertsOnRuleDetailsPage; + + const columns: Array> = [ + { + field: 'id', + name: '', + width: '5%', + render: (id: string, alert: ContextualFlyoutAlertsField) => ( + handleOnEventAlertDetailPanelOpened(id, alert.index, tableId)}> + + + ), + }, + { + field: 'ruleName', + render: (ruleName: string) => {ruleName}, + name: i18n.translate( + 'xpack.securitySolution.flyout.left.insights.alerts.table.ruleNameColumnName', + { + defaultMessage: 'Rule', + } + ), + width: '55%', + }, + { + field: 'severity', + render: (severity: AlertSeverity) => ( + + + + ), + name: i18n.translate( + 'xpack.securitySolution.flyout.left.insights.alerts.table.severityColumnName', + { + defaultMessage: 'Severity', + } + ), + width: '20%', + }, + { + field: 'status', + render: (status: string) => {capitalize(status)}, + name: i18n.translate( + 'xpack.securitySolution.flyout.left.insights.alerts.table.statusColumnName', + { + defaultMessage: 'Status', + } + ), + width: '20%', + }, + ]; + + const openAlertsPageWithFilters = useNavigateToAlertsPageWithFilters(); + + const openAlertsInAlertsPage = useCallback( + () => + openAlertsPageWithFilters( + [ + { + title: + fieldName === 'host.name' + ? OPEN_IN_ALERTS_TITLE_HOSTNAME + : OPEN_IN_ALERTS_TITLE_USERNAME, + selectedOptions: [queryName], + fieldName, + }, + { + title: OPEN_IN_ALERTS_TITLE_STATUS, + selectedOptions: [FILTER_OPEN, FILTER_ACKNOWLEDGED], + fieldName: 'kibana.alert.workflow_status', + }, + ], + true + ), + [fieldName, openAlertsPageWithFilters, queryName] + ); + + return ( + <> + + openAlertsInAlertsPage()}> +

+ {i18n.translate('xpack.securitySolution.flyout.left.insights.alerts.tableTitle', { + defaultMessage: 'Alerts ', + })} + +

+
+ + + + + +
+ + ); + } +); + +AlertsDetailsTable.displayName = 'AlertsDetailsTable'; diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/insights_tab_csp.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/insights_tab_csp.tsx index 05421cfa7a208..2e7b4171fd023 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/insights_tab_csp.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/insights_tab_csp.tsx @@ -12,10 +12,10 @@ import { FormattedMessage } from '@kbn/i18n-react'; import type { FlyoutPanelProps, PanelPath } from '@kbn/expandable-flyout'; import { useExpandableFlyoutState } from '@kbn/expandable-flyout'; import { i18n } from '@kbn/i18n'; -// import type { FlyoutPanels } from '@kbn/expandable-flyout/src/store/state'; import { CspInsightLeftPanelSubTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; import { MisconfigurationFindingsDetailsTable } from './misconfiguration_findings_details_table'; import { VulnerabilitiesFindingsDetailsTable } from './vulnerabilities_findings_details_table'; +import { AlertsDetailsTable } from './alerts_findings_details_table'; /** * Insights view displayed in the document details expandable flyout left section @@ -26,6 +26,7 @@ interface CspFlyoutPanelProps extends FlyoutPanelProps { path: PanelPath; hasMisconfigurationFindings: boolean; hasVulnerabilitiesFindings: boolean; + hasNonClosedAlerts: boolean; }; } @@ -35,7 +36,8 @@ function isCspFlyoutPanelProps( ): panelLeft is CspFlyoutPanelProps { return ( !!panelLeft?.params?.hasMisconfigurationFindings || - !!panelLeft?.params?.hasVulnerabilitiesFindings + !!panelLeft?.params?.hasVulnerabilitiesFindings || + !!panelLeft?.params?.hasNonClosedAlerts ); } @@ -45,12 +47,14 @@ export const InsightsTabCsp = memo( let hasMisconfigurationFindings = false; let hasVulnerabilitiesFindings = false; + let hasNonClosedAlerts = false; let subTab: string | undefined; // Check if panels.left is of type CspFlyoutPanelProps and extract values if (isCspFlyoutPanelProps(panels.left)) { hasMisconfigurationFindings = panels.left.params.hasMisconfigurationFindings; hasVulnerabilitiesFindings = panels.left.params.hasVulnerabilitiesFindings; + hasNonClosedAlerts = panels.left.params.hasNonClosedAlerts; subTab = panels.left.params.path?.subTab; } @@ -63,6 +67,8 @@ export const InsightsTabCsp = memo( ? CspInsightLeftPanelSubTab.MISCONFIGURATIONS : hasVulnerabilitiesFindings ? CspInsightLeftPanelSubTab.VULNERABILITIES + : hasNonClosedAlerts + ? CspInsightLeftPanelSubTab.ALERTS : ''; }; @@ -71,6 +77,19 @@ export const InsightsTabCsp = memo( const insightsButtons: EuiButtonGroupOptionProps[] = useMemo(() => { const buttons: EuiButtonGroupOptionProps[] = []; + if (panels.left?.params?.hasNonClosedAlerts) { + buttons.push({ + id: CspInsightLeftPanelSubTab.ALERTS, + label: ( + + ), + 'data-test-subj': 'alertsTabDataTestId', + }); + } + if (panels.left?.params?.hasMisconfigurationFindings) { buttons.push({ id: CspInsightLeftPanelSubTab.MISCONFIGURATIONS, @@ -96,9 +115,11 @@ export const InsightsTabCsp = memo( 'data-test-subj': 'vulnerabilitiesTabDataTestId', }); } + return buttons; }, [ panels.left?.params?.hasMisconfigurationFindings, + panels.left?.params?.hasNonClosedAlerts, panels.left?.params?.hasVulnerabilitiesFindings, ]); @@ -130,8 +151,10 @@ export const InsightsTabCsp = memo( {activeInsightsId === CspInsightLeftPanelSubTab.MISCONFIGURATIONS ? ( - ) : ( + ) : activeInsightsId === CspInsightLeftPanelSubTab.VULNERABILITIES ? ( + ) : ( + )} ); diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/entity_insight.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/entity_insight.tsx index a43b56876f1ab..7139994f7e972 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/entity_insight.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/entity_insight.tsx @@ -94,7 +94,12 @@ export const EntityInsight = ({ if (alertsCount > 0) { insightContent.push( <> - + ); @@ -103,14 +108,23 @@ export const EntityInsight = ({ if (hasMisconfigurationFindings) insightContent.push( <> - + 0} + isPreviewMode={isPreviewMode} + /> ); if (isVulnerabilitiesFindingForHost && hasVulnerabilitiesFindings) insightContent.push( <> - + 0} + /> ); diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx index b133e9db22050..42a5906ce4e36 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx @@ -103,10 +103,12 @@ const MisconfigurationPreviewScore = ({ export const MisconfigurationsPreview = ({ name, fieldName, + hasNonClosedAlerts = false, isPreviewMode, }: { name: string; fieldName: 'host.name' | 'user.name'; + hasNonClosedAlerts?: boolean; isPreviewMode?: boolean; }) => { const { data } = useMisconfigurationPreview({ @@ -180,6 +182,7 @@ export const MisconfigurationsPreview = ({ isRiskScoreExist, hasMisconfigurationFindings, hasVulnerabilitiesFindings, + hasNonClosedAlerts, path: { tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS, subTab: CspInsightLeftPanelSubTab.MISCONFIGURATIONS, @@ -189,11 +192,16 @@ export const MisconfigurationsPreview = ({ user: { name }, isRiskScoreExist, hasMisconfigurationFindings, - path: { tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS }, + hasNonClosedAlerts, + path: { + tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS, + subTab: CspInsightLeftPanelSubTab.MISCONFIGURATIONS, + }, }, }); }, [ hasMisconfigurationFindings, + hasNonClosedAlerts, hasVulnerabilitiesFindings, isRiskScoreExist, isUsingHostName, diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.tsx index a9ddaff62085b..c4335d921e371 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.tsx @@ -72,9 +72,11 @@ const VulnerabilitiesCount = ({ export const VulnerabilitiesPreview = ({ name, isPreviewMode, + hasNonClosedAlerts = false, }: { name: string; isPreviewMode?: boolean; + hasNonClosedAlerts?: boolean; }) => { useEffect(() => { uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, ENTITY_FLYOUT_WITH_VULNERABILITY_PREVIEW); @@ -132,11 +134,13 @@ export const VulnerabilitiesPreview = ({ isRiskScoreExist, hasMisconfigurationFindings, hasVulnerabilitiesFindings, + hasNonClosedAlerts, path: { tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS, subTab: 'vulnerabilitiesTabId' }, }, }); }, [ hasMisconfigurationFindings, + hasNonClosedAlerts, hasVulnerabilitiesFindings, isRiskScoreExist, name, diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_navigate_to_alerts_page_with_filters.test.ts b/x-pack/plugins/security_solution/public/common/hooks/use_navigate_to_alerts_page_with_filters.test.ts index 6a957318f278f..3bfc0c56e81fa 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_navigate_to_alerts_page_with_filters.test.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_navigate_to_alerts_page_with_filters.test.ts @@ -32,6 +32,7 @@ describe('useNavigateToAlertsPageWithFilters', () => { expect(mockNavigateTo).toHaveBeenCalledWith({ deepLinkId: SecurityPageName.alerts, path: "?pageFilters=!((exclude:!f,existsSelected:!f,fieldName:'test field',hideActionBar:!f,selectedOptions:!('test value'),title:'test filter'))", + openInNewTab: false, }); }); @@ -63,6 +64,7 @@ describe('useNavigateToAlertsPageWithFilters', () => { expect(mockNavigateTo).toHaveBeenCalledWith({ deepLinkId: SecurityPageName.alerts, path: "?pageFilters=!((exclude:!f,existsSelected:!f,fieldName:'test field 1',hideActionBar:!f,selectedOptions:!('test value 1'),title:'test filter 1'),(exclude:!t,existsSelected:!t,fieldName:'test field 2',hideActionBar:!t,selectedOptions:!('test value 2'),title:'test filter 2'))", + openInNewTab: false, }); }); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_navigate_to_alerts_page_with_filters.ts b/x-pack/plugins/security_solution/public/common/hooks/use_navigate_to_alerts_page_with_filters.ts index fffa65797b3f8..037bac32d8c92 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_navigate_to_alerts_page_with_filters.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_navigate_to_alerts_page_with_filters.ts @@ -16,7 +16,7 @@ import { URL_PARAM_KEY } from './use_url_state'; export const useNavigateToAlertsPageWithFilters = () => { const { navigateTo } = useNavigation(); - return (filterItems: FilterControlConfig | FilterControlConfig[]) => { + return (filterItems: FilterControlConfig | FilterControlConfig[], openInNewTab = false) => { const urlFilterParams = encode( formatPageFilterSearchParam(Array.isArray(filterItems) ? filterItems : [filterItems]) ); @@ -24,6 +24,7 @@ export const useNavigateToAlertsPageWithFilters = () => { navigateTo({ deepLinkId: SecurityPageName.alerts, path: `?${URL_PARAM_KEY.pageFilter}=${urlFilterParams}`, + openInNewTab, }); }; }; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx index 6e5774ba1756e..107cb83ddd97b 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx @@ -25,6 +25,7 @@ export interface HostDetailsPanelProps extends Record { scopeId: string; hasMisconfigurationFindings?: boolean; hasVulnerabilitiesFindings?: boolean; + hasNonClosedAlerts?: boolean; path?: { tab?: EntityDetailsLeftPanelTab; subTab?: CspInsightLeftPanelSubTab; @@ -43,6 +44,7 @@ export const HostDetailsPanel = ({ path, hasMisconfigurationFindings, hasVulnerabilitiesFindings, + hasNonClosedAlerts, }: HostDetailsPanelProps) => { const [selectedTabId, setSelectedTabId] = useState( path?.tab === EntityDetailsLeftPanelTab.CSP_INSIGHTS @@ -58,11 +60,18 @@ export const HostDetailsPanel = ({ // Determine if the Insights tab should be included const insightsTab = - hasMisconfigurationFindings || hasVulnerabilitiesFindings + hasMisconfigurationFindings || hasVulnerabilitiesFindings || hasNonClosedAlerts ? [getInsightsInputTab({ name, fieldName: 'host.name' })] : []; return [[...riskScoreTab, ...insightsTab], EntityDetailsLeftPanelTab.RISK_INPUTS, () => {}]; - }, [isRiskScoreExist, name, scopeId, hasMisconfigurationFindings, hasVulnerabilitiesFindings]); + }, [ + isRiskScoreExist, + name, + scopeId, + hasMisconfigurationFindings, + hasVulnerabilitiesFindings, + hasNonClosedAlerts, + ]); return ( <> diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx index 83fa75474a1cc..a7e99898606f8 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx @@ -13,6 +13,8 @@ import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-commo import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; import { sum } from 'lodash'; +import { DETECTION_RESPONSE_ALERTS_BY_STATUS_ID } from '../../../overview/components/detection_response/alerts_by_status/types'; +import { useAlertsByStatus } from '../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status'; import { useRefetchQueryById } from '../../../entity_analytics/api/hooks/use_refetch_query_by_id'; import { RISK_INPUTS_TAB_QUERY_ID } from '../../../entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs_tab'; import type { Refetch } from '../../../common/types'; @@ -35,6 +37,7 @@ import { useObservedHost } from './hooks/use_observed_host'; import { HostDetailsPanelKey } from '../host_details_left'; import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; import { HostPreviewPanelFooter } from '../host_preview/footer'; +import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index'; import { EntityEventTypes } from '../../../common/lib/telemetry'; export interface HostPanelProps extends Record { @@ -120,6 +123,21 @@ export const HostPanel = ({ const hasVulnerabilitiesFindings = sum(Object.values(vulnerabilitiesData?.count || {})) > 0; + const { signalIndexName } = useSignalIndex(); + + const entityFilter = useMemo(() => ({ field: 'host.name', value: hostName }), [hostName]); + + const { items: alertsData } = useAlertsByStatus({ + entityFilter, + signalIndexName, + queryId: `${DETECTION_RESPONSE_ALERTS_BY_STATUS_ID}HOST_NAME_RIGHT`, + to, + from, + }); + + const hasNonClosedAlerts = + (alertsData?.acknowledged?.total || 0) + (alertsData?.open?.total || 0) > 0; + useQueryInspector({ deleteQuery, inspect: inspectRiskScore, @@ -144,6 +162,7 @@ export const HostPanel = ({ path: tab ? { tab } : undefined, hasMisconfigurationFindings, hasVulnerabilitiesFindings, + hasNonClosedAlerts, }, }); }, @@ -155,6 +174,7 @@ export const HostPanel = ({ isRiskScoreExist, hasMisconfigurationFindings, hasVulnerabilitiesFindings, + hasNonClosedAlerts, ] ); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx index 08623c941ba67..254985b865840 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx @@ -28,6 +28,7 @@ export enum EntityDetailsLeftPanelTab { export enum CspInsightLeftPanelSubTab { MISCONFIGURATIONS = 'misconfigurationTabId', VULNERABILITIES = 'vulnerabilitiesTabId', + ALERTS = 'alertsTabId', } export interface PanelHeaderProps { diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx index 8e6cf3a9ee9d2..87c9e5abc7afd 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx @@ -29,6 +29,7 @@ export interface UserDetailsPanelProps extends Record { path?: PanelPath; scopeId: string; hasMisconfigurationFindings?: boolean; + hasNonClosedAlerts?: boolean; } export interface UserDetailsExpandableFlyoutProps extends FlyoutPanelProps { key: 'user_details'; @@ -42,6 +43,7 @@ export const UserDetailsPanel = ({ path, scopeId, hasMisconfigurationFindings, + hasNonClosedAlerts, }: UserDetailsPanelProps) => { const managedUser = useManagedUser(user.name, user.email); const tabs = useTabs( @@ -49,7 +51,8 @@ export const UserDetailsPanel = ({ user.name, isRiskScoreExist, scopeId, - hasMisconfigurationFindings + hasMisconfigurationFindings, + hasNonClosedAlerts ); const { selectedTabId, setSelectedTabId } = useSelectedTab( @@ -57,7 +60,8 @@ export const UserDetailsPanel = ({ user, tabs, path, - hasMisconfigurationFindings + hasMisconfigurationFindings, + hasNonClosedAlerts ); if (managedUser.isLoading) return ; @@ -83,7 +87,8 @@ const useSelectedTab = ( user: UserParam, tabs: LeftPanelTabsType, path: PanelPath | undefined, - hasMisconfigurationFindings?: boolean + hasMisconfigurationFindings?: boolean, + hasNonClosedAlerts?: boolean ) => { const { openLeftPanel } = useExpandableFlyoutApi(); @@ -101,6 +106,7 @@ const useSelectedTab = ( user, isRiskScoreExist, hasMisconfigurationFindings, + hasNonClosedAlerts, path: { tab: tabId, }, diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx index 6f27b054759f2..0c1cdcaa904a9 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx @@ -30,7 +30,8 @@ export const useTabs = ( name: string, isRiskScoreExist: boolean, scopeId: string, - hasMisconfigurationFindings?: boolean + hasMisconfigurationFindings?: boolean, + hasNonClosedAlerts?: boolean ): LeftPanelTabsType => useMemo(() => { const tabs: LeftPanelTabsType = []; @@ -55,12 +56,19 @@ export const useTabs = ( tabs.push(getEntraTab(entraManagedUser)); } - if (hasMisconfigurationFindings) { + if (hasMisconfigurationFindings || hasNonClosedAlerts) { tabs.push(getInsightsInputTab({ name, fieldName: 'user.name' })); } return tabs; - }, [hasMisconfigurationFindings, isRiskScoreExist, managedUser, name, scopeId]); + }, [ + hasMisconfigurationFindings, + hasNonClosedAlerts, + isRiskScoreExist, + managedUser, + name, + scopeId, + ]); const getOktaTab = (oktaManagedUser: ManagedUserHit) => ({ id: EntityDetailsLeftPanelTab.OKTA, diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx index 42c8664b2ac0c..07762ed9aea0c 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx @@ -33,6 +33,9 @@ import { UserDetailsPanelKey } from '../user_details_left'; import { useObservedUser } from './hooks/use_observed_user'; import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; import { UserPreviewPanelFooter } from '../user_preview/footer'; +import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index'; +import { useAlertsByStatus } from '../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status'; +import { DETECTION_RESPONSE_ALERTS_BY_STATUS_ID } from '../../../overview/components/detection_response/alerts_by_status/types'; import { EntityEventTypes } from '../../../common/lib/telemetry'; export interface UserPanelProps extends Record { @@ -112,6 +115,21 @@ export const UserPanel = ({ const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0; + const { signalIndexName } = useSignalIndex(); + + const entityFilter = useMemo(() => ({ field: 'user.name', value: userName }), [userName]); + + const { items: alertsData } = useAlertsByStatus({ + entityFilter, + signalIndexName, + queryId: `${DETECTION_RESPONSE_ALERTS_BY_STATUS_ID}USER_NAME_RIGHT`, + to, + from, + }); + + const hasNonClosedAlerts = + (alertsData?.acknowledged?.total || 0) + (alertsData?.open?.total || 0) > 0; + useQueryInspector({ deleteQuery, inspect, @@ -139,6 +157,7 @@ export const UserPanel = ({ }, path: tab ? { tab } : undefined, hasMisconfigurationFindings, + hasNonClosedAlerts, }, }); }, @@ -150,6 +169,7 @@ export const UserPanel = ({ userName, email, hasMisconfigurationFindings, + hasNonClosedAlerts, ] ); const openPanelFirstTab = useCallback( @@ -191,7 +211,8 @@ export const UserPanel = ({ <> diff --git a/x-pack/plugins/session_view/server/routes/alert_status_route.ts b/x-pack/plugins/session_view/server/routes/alert_status_route.ts index e0b95f9705e9d..64192198b5e46 100644 --- a/x-pack/plugins/session_view/server/routes/alert_status_route.ts +++ b/x-pack/plugins/session_view/server/routes/alert_status_route.ts @@ -31,6 +31,11 @@ export const registerAlertStatusRoute = ( .addVersion( { version: '1', + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, validate: { request: { query: schema.object({ diff --git a/x-pack/plugins/session_view/server/routes/alerts_route.ts b/x-pack/plugins/session_view/server/routes/alerts_route.ts index c6b7fd8db7896..c875236989efe 100644 --- a/x-pack/plugins/session_view/server/routes/alerts_route.ts +++ b/x-pack/plugins/session_view/server/routes/alerts_route.ts @@ -36,6 +36,11 @@ export const registerAlertsRoute = ( .addVersion( { version: '1', + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, validate: { request: { query: schema.object({ diff --git a/x-pack/plugins/session_view/server/routes/get_total_io_bytes_route.ts b/x-pack/plugins/session_view/server/routes/get_total_io_bytes_route.ts index 50f36ac47f5a4..7d54654c89cdc 100644 --- a/x-pack/plugins/session_view/server/routes/get_total_io_bytes_route.ts +++ b/x-pack/plugins/session_view/server/routes/get_total_io_bytes_route.ts @@ -22,6 +22,12 @@ export const registerGetTotalIOBytesRoute = (router: IRouter, logger: Logger) => .addVersion( { version: '1', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Elasticsearch and it's not tied to a Kibana privilege.`, + }, + }, validate: { request: { query: schema.object({ diff --git a/x-pack/plugins/session_view/server/routes/io_events_route.ts b/x-pack/plugins/session_view/server/routes/io_events_route.ts index 9810f9da5aa77..3e73517a978c3 100644 --- a/x-pack/plugins/session_view/server/routes/io_events_route.ts +++ b/x-pack/plugins/session_view/server/routes/io_events_route.ts @@ -29,6 +29,12 @@ export const registerIOEventsRoute = (router: IRouter, logger: Logger) => { .addVersion( { version: '1', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Elasticsearch and it's not tied to a Kibana privilege.`, + }, + }, validate: { request: { query: schema.object({ diff --git a/x-pack/plugins/session_view/server/routes/process_events_route.ts b/x-pack/plugins/session_view/server/routes/process_events_route.ts index bc6b24fc36bc5..b30b3b6ddcc51 100644 --- a/x-pack/plugins/session_view/server/routes/process_events_route.ts +++ b/x-pack/plugins/session_view/server/routes/process_events_route.ts @@ -43,6 +43,12 @@ export const registerProcessEventsRoute = ( .addVersion( { version: '1', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Elasticsearch and it's not tied to a Kibana privilege.`, + }, + }, validate: { request: { query: schema.object({ diff --git a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx index 1c97b9c4d2bc6..d35bdbee4b8bb 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx @@ -6,7 +6,7 @@ */ import { EuiThemeProvider } from '@elastic/eui'; -import { render, screen, waitFor, within } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import crypto from 'crypto'; import React from 'react'; @@ -142,6 +142,15 @@ describe('PrivilegesRolesForm', () => { jest.clearAllMocks(); }); + it("would open the 'manage roles' link in a new tab", () => { + getRolesSpy.mockResolvedValue([]); + getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); + + renderPrivilegeRolesForm(); + + expect(screen.getByText('Manage roles')).toHaveAttribute('target', '_blank'); + }); + it('does not display the privilege selection buttons or customization form when no role is selected', async () => { getRolesSpy.mockResolvedValue([]); getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); @@ -170,6 +179,23 @@ describe('PrivilegesRolesForm', () => { expect(screen.getByTestId('space-assign-role-create-roles-privilege-button')).toBeDisabled(); }); + it('makes a request to refetch available roles if page transitions back from a user interaction page visibility change', () => { + getRolesSpy.mockResolvedValue([]); + getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); + + renderPrivilegeRolesForm(); + + expect(getRolesSpy).toHaveBeenCalledTimes(1); + + // trigger click on manage roles link, which is perquisite for page visibility handler to trigger role refetch + fireEvent.click(screen.getByText(/manage roles/i)); + + // trigger page visibility change + fireEvent(document, new Event('visibilitychange')); + + expect(getRolesSpy).toHaveBeenCalledTimes(2); + }); + it('renders with the assign roles button disabled when no base privileges or feature privileges are selected', async () => { getRolesSpy.mockResolvedValue([]); getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); diff --git a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx index e0f3e8f3714c6..74f2b2fde4667 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx @@ -23,6 +23,7 @@ import { EuiSpacer, EuiText, EuiTitle, + useGeneratedHtmlId, } from '@elastic/eui'; import type { EuiComboBoxOptionOption } from '@elastic/eui'; import type { FC } from 'react'; @@ -81,10 +82,13 @@ export const PrivilegesRolesForm: FC = (props) => { const [selectedRoles, setSelectedRoles] = useState>( createRolesComboBoxOptions(defaultSelected) ); + const manageRoleLinkId = useGeneratedHtmlId(); const isEditOperation = useRef(Boolean(defaultSelected.length)); - useEffect(() => { - async function fetchRequiredData(spaceId: string) { + const userInvokedPageVisibilityChange = useRef(null); + + const fetchRequiredData = useCallback( + async (spaceId: string) => { setFetchingDataDeps(true); const [systemRoles, _kibanaPrivileges] = await invokeClient((clients) => @@ -109,10 +113,28 @@ export const PrivilegesRolesForm: FC = (props) => { ); setKibanaPrivileges(_kibanaPrivileges); - } + }, + [invokeClient] + ); + useEffect(() => { fetchRequiredData(space.id!).finally(() => setFetchingDataDeps(false)); - }, [invokeClient, space.id]); + }, [fetchRequiredData, invokeClient, space.id]); + + useEffect(() => { + async function visibilityChangeHandler() { + // page just transitioned back to visible state from hidden state caused by user interaction + if (userInvokedPageVisibilityChange.current && !document.hidden) { + await fetchRequiredData(space.id!).finally(() => setFetchingDataDeps(false)); + } + } + + document.addEventListener('visibilitychange', visibilityChangeHandler); + + return () => { + document.removeEventListener('visibilitychange', visibilityChangeHandler); + }; + }, [fetchRequiredData, invokeClient, space.id]); const selectedRolesCombinedPrivileges = useMemo(() => { const combinedPrivilege = new Set( @@ -345,12 +367,23 @@ export const PrivilegesRolesForm: FC = (props) => { {!isEditOperation.current && ( ) => { + // leverage event propagation, check if manage role link element was clicked + if ((event.target as HTMLFieldSetElement).id === manageRoleLinkId) { + userInvokedPageVisibilityChange.current = true; + } + }} label={i18n.translate( 'xpack.spaces.management.spaceDetails.roles.selectRolesFormRowLabel', { defaultMessage: 'Select roles' } )} labelAppend={ - + {i18n.translate( 'xpack.spaces.management.spaceDetails.roles.selectRolesFormRowLabelAnchor', { defaultMessage: 'Manage roles' } diff --git a/x-pack/test/api_integration/apis/management/index_management/lib/templates.helpers.ts b/x-pack/test/api_integration/apis/management/index_management/lib/templates.helpers.ts index ea1e9a2d83bb1..a342df2d6287e 100644 --- a/x-pack/test/api_integration/apis/management/index_management/lib/templates.helpers.ts +++ b/x-pack/test/api_integration/apis/management/index_management/lib/templates.helpers.ts @@ -59,6 +59,7 @@ export function templatesHelpers(getService: FtrProviderContext['getService']) { name, indexPatterns, version: 1, + indexMode: 'standard', template: { ...getTemplateMock(isMappingsSourceFieldEnabled) }, _kbnMeta: { isLegacy, diff --git a/x-pack/test/api_integration/apis/management/index_management/templates.ts b/x-pack/test/api_integration/apis/management/index_management/templates.ts index 1fe7e022bfc9a..066df3120be08 100644 --- a/x-pack/test/api_integration/apis/management/index_management/templates.ts +++ b/x-pack/test/api_integration/apis/management/index_management/templates.ts @@ -92,6 +92,7 @@ export default function ({ getService }: FtrProviderContext) { const expectedKeys = [ 'name', 'indexPatterns', + 'indexMode', 'hasSettings', 'hasAliases', 'hasMappings', @@ -115,6 +116,7 @@ export default function ({ getService }: FtrProviderContext) { const expectedLegacyKeys = [ 'name', 'indexPatterns', + 'indexMode', 'hasSettings', 'hasAliases', 'hasMappings', @@ -138,6 +140,7 @@ export default function ({ getService }: FtrProviderContext) { const expectedWithDSLKeys = [ 'name', 'indexPatterns', + 'indexMode', 'lifecycle', 'hasSettings', 'hasAliases', @@ -163,6 +166,7 @@ export default function ({ getService }: FtrProviderContext) { const expectedWithILMKeys = [ 'name', 'indexPatterns', + 'indexMode', 'ilmPolicy', 'hasSettings', 'hasAliases', @@ -190,6 +194,7 @@ export default function ({ getService }: FtrProviderContext) { const expectedKeys = [ 'name', 'indexPatterns', + 'indexMode', 'template', 'composedOf', 'ignoreMissingComponentTemplates', @@ -213,6 +218,7 @@ export default function ({ getService }: FtrProviderContext) { const expectedKeys = [ 'name', 'indexPatterns', + 'indexMode', 'template', 'order', 'version', @@ -375,6 +381,7 @@ export default function ({ getService }: FtrProviderContext) { _kbnMeta: { hasDatastream: false, type: 'default' }, name: templateName, indexPatterns: [getRandomString()], + indexMode: 'standard', template: {}, deprecated: true, allowAutoCreate: 'TRUE', diff --git a/x-pack/test/api_integration/apis/synthetics/get_monitor_project.ts b/x-pack/test/api_integration/apis/synthetics/get_monitor_project.ts index 53089d2bec2d3..dd3fcbef3088d 100644 --- a/x-pack/test/api_integration/apis/synthetics/get_monitor_project.ts +++ b/x-pack/test/api_integration/apis/synthetics/get_monitor_project.ts @@ -95,6 +95,9 @@ export default function ({ getService }: FtrProviderContext) { const firstPageResponse = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS_PROJECT.replace('{projectName}', project)) + .query({ + per_page: 500, + }) .set('kbn-xsrf', 'true') .send() .expect(200); @@ -185,6 +188,9 @@ export default function ({ getService }: FtrProviderContext) { const firstPageResponse = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS_PROJECT.replace('{projectName}', project)) + .query({ + per_page: 500, + }) .set('kbn-xsrf', 'true') .send() .expect(200); @@ -279,6 +285,9 @@ export default function ({ getService }: FtrProviderContext) { const firstPageResponse = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS_PROJECT.replace('{projectName}', project)) + .query({ + per_page: 500, + }) .set('kbn-xsrf', 'true') .send() .expect(200); @@ -372,6 +381,9 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); const firstPageResponse = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS_PROJECT.replace('{projectName}', project)) + .query({ + per_page: 500, + }) .set('kbn-xsrf', 'true') .send() .expect(200); @@ -481,6 +493,9 @@ export default function ({ getService }: FtrProviderContext) { encodeURI(projectName) ) ) + .query({ + per_page: 500, + }) .set('kbn-xsrf', 'true') .send() .expect(200); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/agent_explorer/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/agent_explorer/index.ts index f77b13923930a..ae05cb6b89cbe 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/agent_explorer/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/agent_explorer/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('agent_explorer', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts index e0b9e1b022b4f..c515263f09b2e 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts @@ -10,7 +10,7 @@ import { errorCountActionVariables } from '@kbn/apm-plugin/server/routes/alerts/ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { omit } from 'lodash'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import type { RoleCredentials } from '../../../../services'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/index.ts index 71661e4cbc8bc..3796b4253e60e 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('alerts', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_count.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_count.spec.ts index d6792400fc2bc..38bf9d4eade4d 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_count.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_count.spec.ts @@ -7,7 +7,7 @@ import type { PreviewChartResponseItem } from '@kbn/apm-plugin/server/routes/alerts/route'; import expect from '@kbn/expect'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { ERROR_GROUP_ID, SERVICE_ENVIRONMENT, diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_rate.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_rate.spec.ts index 3e5c0753fbc1d..167b114e755c3 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_rate.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_rate.spec.ts @@ -13,7 +13,7 @@ import { } from '@kbn/observability-shared-plugin/common'; import type { PreviewChartResponseItem } from '@kbn/apm-plugin/server/routes/alerts/route'; import expect from '@kbn/expect'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { generateErrorData } from './generate_data'; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/cold_start/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/cold_start/index.ts index a5d8045f227d3..108340514a0c2 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/cold_start/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/cold_start/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('cold_start', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/index.ts index 660556edb8d79..ffa54dbe7bb5c 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('correlations', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/custom_dashboards/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/custom_dashboards/index.ts index 77f12ca0f88d5..a46024ff1ab10 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/custom_dashboards/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/custom_dashboards/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('custom_dashboards', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/data_view/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/data_view/index.ts index 9412ddca7cbcb..c81b643e62638 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/data_view/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/data_view/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('data_view', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/index.ts index 2acf449ce923d..46ad399380550 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('custom_dashboards', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_dependencies.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_dependencies.spec.ts index 0fa88b67d3379..21e990a1dbb52 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_dependencies.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_dependencies.spec.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import { NodeType, DependencyNode } from '@kbn/apm-plugin/common/connections'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { dataConfig, generateData } from './generate_data'; import { roundNumber } from '../utils/common'; diff --git a/x-pack/test/apm_api_integration/tests/diagnostics/apm_events.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/apm_events.spec.ts similarity index 93% rename from x-pack/test/apm_api_integration/tests/diagnostics/apm_events.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/apm_events.spec.ts index 1abace6c77f4c..4ca60e15fa190 100644 --- a/x-pack/test/apm_api_integration/tests/diagnostics/apm_events.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/apm_events.spec.ts @@ -9,19 +9,18 @@ import expect from '@kbn/expect'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import { sumBy } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); const es = getService('es'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); + const synthtrace = getService('synthtrace'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - // FLAKY: https://github.com/elastic/kibana/issues/177144 - registry.when('Diagnostics: APM Events', { config: 'basic', archives: [] }, () => { + describe('Diagnostics: APM Events', () => { describe('When there is no data', () => { before(async () => { // delete APM data streams @@ -38,10 +37,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('When data is ingested', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { const instance = apm .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) .instance('instance-a'); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await apmSynthtraceEsClient.index( timerange(start, end) diff --git a/x-pack/test/apm_api_integration/tests/diagnostics/data_streams.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/data_streams.spec.ts similarity index 82% rename from x-pack/test/apm_api_integration/tests/diagnostics/data_streams.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/data_streams.spec.ts index 80fa34dbaa002..1d37c668e91f5 100644 --- a/x-pack/test/apm_api_integration/tests/diagnostics/data_streams.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/data_streams.spec.ts @@ -7,20 +7,18 @@ import expect from '@kbn/expect'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); const es = getService('es'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - const synthtraceKibanaClient = getService('synthtraceKibanaClient'); + const synthtrace = getService('synthtrace'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - // FLAKY: https://github.com/elastic/kibana/issues/177245 - registry.when('Diagnostics: Data streams', { config: 'basic', archives: [] }, () => { + describe('Diagnostics: Data streams', () => { describe('When there is no data', () => { before(async () => { // delete APM data streams @@ -45,9 +43,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('When data is ingested', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { - const latestVersion = await synthtraceKibanaClient.fetchLatestApmPackageVersion(); - await synthtraceKibanaClient.installApmPackage(latestVersion); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); const instance = apm .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/index.ts new file mode 100644 index 0000000000000..192771d3fc75e --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('diagnostics', () => { + loadTestFile(require.resolve('./apm_events.spec.ts')); + loadTestFile(require.resolve('./data_streams.spec.ts')); + loadTestFile(require.resolve('./index_pattern_settings.spec.ts')); + loadTestFile(require.resolve('./index_templates.spec.ts')); + loadTestFile(require.resolve('./indices.spec.ts')); + loadTestFile(require.resolve('./privileges.spec.ts')); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/index_pattern_settings.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/index_pattern_settings.spec.ts new file mode 100644 index 0000000000000..8235e2a179d4c --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/index_pattern_settings.spec.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { apm, timerange } from '@kbn/apm-synthtrace-client'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import { uniq } from 'lodash'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + + describe('Diagnostics: Index pattern settings', () => { + describe('When data is ingested', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + + const instance = apm + .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) + .instance('instance-a'); + await apmSynthtraceEsClient.index( + timerange(start, end) + .interval('1m') + .rate(30) + .generator((timestamp) => + instance + .transaction({ transactionName: 'GET /users' }) + .timestamp(timestamp) + .duration(100) + .success() + ) + ); + }); + + after(() => apmSynthtraceEsClient.clean()); + + it('returns APM index templates', async () => { + const apmIndexTemplatesPatterns = ['apm', 'otel']; + + const { status, body } = await apmApiClient.adminUser({ + endpoint: 'GET /internal/apm/diagnostics', + }); + expect(status).to.be(200); + + // filtering the array for unique index templates because they get duplicated across different index patterns + const uniqueTemplateNames = uniq( + body.indexTemplatesByIndexPattern.flatMap(({ indexTemplates }) => { + return indexTemplates?.map(({ templateName }) => templateName); + }) + ); + + // filter only APM releated indices + const apmTemplateNames = uniqueTemplateNames.filter( + (templateName) => + templateName.endsWith('@template') && + apmIndexTemplatesPatterns.some((pattern) => templateName.includes(pattern)) + ); + + // sort alphabeticaly before comparing because an order is different between testing environments + const sortedApmTemplates = apmTemplateNames.sort(); + + expect(sortedApmTemplates).to.eql([ + 'logs-apm.app@template', + 'logs-apm.error@template', + 'logs-otel@template', + 'metrics-apm.app@template', + 'metrics-apm.internal@template', + 'metrics-apm.service_destination.10m@template', + 'metrics-apm.service_destination.1m@template', + 'metrics-apm.service_destination.60m@template', + 'metrics-apm.service_summary.10m@template', + 'metrics-apm.service_summary.1m@template', + 'metrics-apm.service_summary.60m@template', + 'metrics-apm.service_transaction.10m@template', + 'metrics-apm.service_transaction.1m@template', + 'metrics-apm.service_transaction.60m@template', + 'metrics-apm.transaction.10m@template', + 'metrics-apm.transaction.1m@template', + 'metrics-apm.transaction.60m@template', + 'metrics-otel@template', + 'metrics-service_summary.10m.otel@template', + 'metrics-service_summary.1m.otel@template', + 'metrics-service_summary.60m.otel@template', + 'metrics-service_transaction.10m.otel@template', + 'metrics-service_transaction.1m.otel@template', + 'metrics-service_transaction.60m.otel@template', + 'metrics-transaction.10m.otel@template', + 'metrics-transaction.1m.otel@template', + 'metrics-transaction.60m.otel@template', + 'traces-apm.rum@template', + 'traces-apm.sampled@template', + 'traces-apm@template', + 'traces-otel@template', + ]); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/diagnostics/index_templates.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/index_templates.spec.ts similarity index 51% rename from x-pack/test/apm_api_integration/tests/diagnostics/index_templates.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/index_templates.spec.ts index 1bbc799b3bf78..aa45a93a3ce73 100644 --- a/x-pack/test/apm_api_integration/tests/diagnostics/index_templates.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/index_templates.spec.ts @@ -7,44 +7,22 @@ import expect from '@kbn/expect'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import { getApmIndexTemplateNames } from '@kbn/apm-plugin/server/routes/diagnostics/helpers/get_apm_index_template_names'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const es = getService('es'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - const synthtraceKibanaClient = getService('synthtraceKibanaClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - registry.when.skip('Diagnostics: Index Templates', { config: 'basic', archives: [] }, () => { - describe('When there is no data', () => { - before(async () => { - // delete APM index templates - await es.indices.deleteIndexTemplate({ - name: Object.values(getApmIndexTemplateNames()).flat(), - }); - }); - - it('verifies that none of the default APM index templates exists`', async () => { - const { status, body } = await apmApiClient.adminUser({ - endpoint: 'GET /internal/apm/diagnostics', - }); - expect(status).to.be(200); - const noApmIndexTemplateExists = body.apmIndexTemplates.every( - ({ exists }) => exists === false - ); - expect(noApmIndexTemplateExists).to.eql(true); - }); - }); - + describe('Diagnostics: Index Templates', () => { describe('When data is ingested', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { - const latestVersion = await synthtraceKibanaClient.fetchLatestApmPackageVersion(); - await synthtraceKibanaClient.installApmPackage(latestVersion); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); const instance = apm .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) diff --git a/x-pack/test/apm_api_integration/tests/diagnostics/indices.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/indices.spec.ts similarity index 88% rename from x-pack/test/apm_api_integration/tests/diagnostics/indices.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/indices.spec.ts index 477824524b48c..92976e6bce883 100644 --- a/x-pack/test/apm_api_integration/tests/diagnostics/indices.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/indices.spec.ts @@ -8,20 +8,20 @@ import expect from '@kbn/expect'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; import { omit } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const es = getService('es'); - const synthtraceKibanaClient = getService('synthtraceKibanaClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - // FLAKY: https://github.com/elastic/kibana/pull/177039 - registry.when.skip('Diagnostics: Indices', { config: 'basic', archives: [] }, () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + describe('Diagnostics: Indices', () => { describe.skip('When there is no data', () => { it('returns empty response`', async () => { const { status, body } = await apmApiClient.adminUser({ @@ -39,6 +39,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const instance = apm .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) .instance('instance-a'); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await apmSynthtraceEsClient.index( timerange(start, end) @@ -92,8 +93,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { after(async () => { await es.indices.delete({ index: 'traces-apm-default' }); - const latestVersion = await synthtraceKibanaClient.fetchLatestApmPackageVersion(); - await synthtraceKibanaClient.installApmPackage(latestVersion); await apmSynthtraceEsClient.clean(); }); @@ -136,8 +135,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); after(async () => { - const latestVersion = await synthtraceKibanaClient.fetchLatestApmPackageVersion(); - await synthtraceKibanaClient.installApmPackage(latestVersion); await apmSynthtraceEsClient.clean(); }); diff --git a/x-pack/test/apm_api_integration/tests/diagnostics/privileges.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/privileges.spec.ts similarity index 90% rename from x-pack/test/apm_api_integration/tests/diagnostics/privileges.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/privileges.spec.ts index 2d9652b612010..fa46ff08cc8ed 100644 --- a/x-pack/test/apm_api_integration/tests/diagnostics/privileges.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/privileges.spec.ts @@ -7,13 +7,12 @@ import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); - registry.when('Diagnostics: Privileges', { config: 'basic', archives: [] }, () => { + describe('Diagnostics: Privileges', () => { describe('superuser', () => { let body: APIReturnType<'GET /internal/apm/diagnostics'>; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/index.ts index d7a36e3e447b7..71be4955fe523 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('entities', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/environment/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/environment/index.ts index 4a77e610d5000..e48a8f88d74c4 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/environment/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/environment/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('environment', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/index.ts index a3dd89f0ddb1a..04ed9b3be1ff0 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('error_rate', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/service_apis.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/service_apis.spec.ts index 56dded824a32d..6a8bca2eefaca 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/service_apis.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/service_apis.spec.ts @@ -13,7 +13,7 @@ import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; const GO_PROD_LIST_RATE = 75; const GO_PROD_LIST_ERROR_RATE = 25; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/service_maps.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/service_maps.spec.ts index 462ad8db4bdda..c45f3fd9457d8 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/service_maps.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/service_maps.spec.ts @@ -5,13 +5,13 @@ * 2.0. */ import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import expect from '@kbn/expect'; import { meanBy } from 'lodash'; import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; const GO_PROD_LIST_RATE = 75; const GO_PROD_LIST_ERROR_RATE = 25; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/historical_data/has_data.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/historical_data/has_data.spec.ts index 6ac96b8e38154..b6eaca65cad1c 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/historical_data/has_data.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/historical_data/has_data.spec.ts @@ -8,8 +8,8 @@ import expect from '@kbn/expect'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; import moment from 'moment'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { const apmApiClient = getService('apmApi'); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/historical_data/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/historical_data/index.ts index 49f0068ee313b..0b9c76d21d3b4 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/historical_data/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/historical_data/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('historical_data', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts index af8cc32783c20..b46b2c3f10637 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; export default function apmApiIntegrationTests({ loadTestFile, @@ -23,6 +23,7 @@ export default function apmApiIntegrationTests({ loadTestFile(require.resolve('./correlations')); loadTestFile(require.resolve('./entities')); loadTestFile(require.resolve('./cold_start')); + loadTestFile(require.resolve('./metrics')); loadTestFile(require.resolve('./services')); loadTestFile(require.resolve('./historical_data')); loadTestFile(require.resolve('./observability_overview')); @@ -30,5 +31,7 @@ export default function apmApiIntegrationTests({ loadTestFile(require.resolve('./infrastructure')); loadTestFile(require.resolve('./inspect')); loadTestFile(require.resolve('./service_groups')); + loadTestFile(require.resolve('./diagnostics')); + loadTestFile(require.resolve('./service_nodes')); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/infrastructure/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/infrastructure/index.ts index 1351a44d41c8e..3379e3f0fdc23 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/infrastructure/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/infrastructure/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('infrastructure', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/infrastructure/infrastructure_attributes.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/infrastructure/infrastructure_attributes.spec.ts index ca70d8d1fa002..fe9b0246c505b 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/infrastructure/infrastructure_attributes.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/infrastructure/infrastructure_attributes.spec.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { generateData } from './generate_data'; -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { const apmApiClient = getService('apmApi'); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/index.ts index 0b9a71293f687..2ff9ec1896c78 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('latency', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/service_apis.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/service_apis.spec.ts index dee5f27b7a61d..2219a873c0f2d 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/service_apis.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/service_apis.spec.ts @@ -12,8 +12,8 @@ import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { const apmApiClient = getService('apmApi'); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/service_maps.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/service_maps.spec.ts index fa088e4f12dc9..e0bb69306a03f 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/service_maps.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/service_maps.spec.ts @@ -10,8 +10,8 @@ import { meanBy } from 'lodash'; import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { const apmApiClient = getService('apmApi'); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/index.ts new file mode 100644 index 0000000000000..454e3b16e9aad --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('metrics', () => { + loadTestFile(require.resolve('./metrics_charts.spec.ts')); + loadTestFile(require.resolve('./memory/memory_metrics.spec.ts')); + loadTestFile(require.resolve('./serverless/serverless_active_instances.spec.ts')); + loadTestFile(require.resolve('./serverless/serverless_functions_overview.spec.ts')); + loadTestFile(require.resolve('./serverless/serverless_metrics_charts.spec.ts')); + loadTestFile(require.resolve('./serverless/serverless_summary.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/metrics/memory/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/memory/generate_data.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/metrics/memory/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/memory/generate_data.ts diff --git a/x-pack/test/apm_api_integration/tests/metrics/memory/memory_metrics.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/memory/memory_metrics.spec.ts similarity index 85% rename from x-pack/test/apm_api_integration/tests/metrics/memory/memory_metrics.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/memory/memory_metrics.spec.ts index e0f8fc1cf28c2..0bcfff6395fef 100644 --- a/x-pack/test/apm_api_integration/tests/metrics/memory/memory_metrics.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/memory/memory_metrics.spec.ts @@ -6,13 +6,13 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { config, generateData } from './generate_data'; +import { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = new Date('2023-01-01T00:00:00.000Z').getTime(); const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1; @@ -33,9 +33,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } - // FLAKY: https://github.com/elastic/kibana/issues/176990 - registry.when('Memory', { config: 'trial', archives: [] }, () => { + describe('Memory', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await generateData({ start, end, apmSynthtraceEsClient }); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/metrics_charts.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/metrics_charts.spec.ts new file mode 100644 index 0000000000000..f801113fdf823 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/metrics_charts.spec.ts @@ -0,0 +1,461 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { first } from 'lodash'; +import { GenericMetricsChart } from '@kbn/apm-plugin/server/routes/metrics/fetch_and_transform_metrics'; +import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { ARCHIVER_ROUTES } from '../constants/archiver'; +import { SupertestReturnType } from '../../../../services/apm_api'; + +type ChartResponse = SupertestReturnType<'GET /internal/apm/services/{serviceName}/metrics/charts'>; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const esArchiver = getService('esArchiver'); + + describe('Metrics charts when data is loaded', () => { + before(async () => { + await esArchiver.load(ARCHIVER_ROUTES['metrics_8.0.0']); + }); + + after(async () => { + await esArchiver.unload(ARCHIVER_ROUTES['metrics_8.0.0']); + }); + + describe('for opbeans-node', () => { + describe('returns metrics data', () => { + let chartsResponse: ChartResponse; + before(async () => { + chartsResponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/metrics/charts', + params: { + path: { serviceName: 'opbeans-node' }, + query: { + start: '2020-09-08T14:50:00.000Z', + end: '2020-09-08T14:55:00.000Z', + agentName: 'nodejs', + environment: 'ENVIRONMENT_ALL', + kuery: ``, + }, + }, + }); + }); + + it('contains CPU usage and System memory usage chart data', async () => { + expect(chartsResponse.status).to.be(200); + expectSnapshot(chartsResponse.body.charts.map((chart) => chart.title)).toMatchInline(` + Array [ + "CPU usage", + "System memory usage", + ] + `); + }); + + describe('CPU usage', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find(({ key }) => key === 'cpu_usage_chart'); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "System max", + "System average", + "Process max", + "Process average", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0.714, + 0.3877, + 0.75, + 0.2543, + ] + `); + }); + }); + + describe("System memory usage (using 'system.memory' fields to calculate the memory usage)", () => { + let systemMemoryUsageChart: GenericMetricsChart | undefined; + before(() => { + systemMemoryUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'memory_usage_chart' + ); + }); + + it('has correct series', () => { + expect(systemMemoryUsageChart).to.not.empty(); + expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Max", + "Average", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0.722093920925555, + 0.718173546796348, + ] + `); + }); + }); + }); + }); + + describe('for opbeans-java', () => { + describe('returns metrics data', () => { + let chartsResponse: ChartResponse; + before(async () => { + chartsResponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/metrics/charts', + params: { + path: { serviceName: 'opbeans-java' }, + query: { + start: '2020-09-08T14:55:30.000Z', + end: '2020-09-08T15:00:00.000Z', + agentName: 'java', + environment: 'ENVIRONMENT_ALL', + kuery: ``, + }, + }, + }); + }); + + it('has correct chart data', async () => { + expect(chartsResponse.status).to.be(200); + expectSnapshot(chartsResponse.body.charts.map((chart) => chart.title)).toMatchInline(` + Array [ + "CPU usage", + "System memory usage", + "Heap Memory", + "Non-Heap Memory", + "Thread Count", + "Garbage collection per minute", + "Garbage collection time spent per minute", + ] + `); + }); + + describe('CPU usage', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find(({ key }) => key === 'cpu_usage_chart'); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "System max", + "System average", + "Process max", + "Process average", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0.203, + 0.178777777777778, + 0.01, + 0.009, + ] + `); + }); + + it('has the correct rate', async () => { + const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 0.193, + 0.193, + 0.009, + 0.009, + ] + `); + }); + }); + + describe("System memory usage (using 'system.process.cgroup' fields to calculate the memory usage)", () => { + let systemMemoryUsageChart: GenericMetricsChart | undefined; + before(() => { + systemMemoryUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'memory_usage_chart' + ); + }); + + it('has correct series', () => { + expect(systemMemoryUsageChart).to.not.empty(); + expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Max", + "Average", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0.707924703557837, + 0.705395980841182, + ] + `); + }); + + it('has the correct rate', async () => { + const yValues = systemMemoryUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 0.707924703557837, + 0.707924703557837, + ] + `); + }); + }); + + describe('Heap Memory', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'heap_memory_area_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Avg. used", + "Avg. committed", + "Avg. limit", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 222501617.777778, + 374341632, + 1560281088, + ] + `); + }); + + it('has the correct rate', async () => { + const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 211472896, + 374341632, + 1560281088, + ] + `); + }); + }); + + describe('Non-Heap Memory', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'non_heap_memory_area_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Avg. used", + "Avg. committed", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 138573397.333333, + 147677639.111111, + ] + `); + }); + + it('has the correct rate', async () => { + const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 138162752, + 147386368, + ] + `); + }); + }); + + describe('Thread Count', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'thread_count_line_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Avg. count", + "Max count", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 44.4444444444444, + 45, + ] + `); + }); + + it('has the correct rate', async () => { + const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 44, + 44, + ] + `); + }); + }); + + describe('Garbage collection per minute', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'gc_rate_line_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "G1 Old Generation", + "G1 Young Generation", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0, + 3, + ] + `); + }); + }); + + describe('Garbage collection time spent per minute', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'gc_time_line_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "G1 Old Generation", + "G1 Young Generation", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0, + 37500, + ] + `); + }); + }); + }); + + // 9223372036854771712 = memory limit for a c-group when no memory limit is specified + it('calculates system memory usage using system total field when cgroup limit is equal to 9223372036854771712', async () => { + const chartsResponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/metrics/charts', + params: { + path: { serviceName: 'opbeans-java' }, + query: { + start: '2020-09-08T15:00:30.000Z', + end: '2020-09-08T15:05:00.000Z', + agentName: 'java', + environment: 'ENVIRONMENT_ALL', + kuery: ``, + }, + }, + }); + + const systemMemoryUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'memory_usage_chart' + ); + + expect(systemMemoryUsageChart).to.not.empty(); + expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Max", + "Average", + ] + `); + expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0.114523896426499, + 0.114002376090415, + ] + `); + + const yValues = systemMemoryUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 0.11383724014064, + 0.11383724014064, + ] + `); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/metrics/serverless/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/generate_data.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/metrics/serverless/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/generate_data.ts diff --git a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_active_instances.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_active_instances.spec.ts similarity index 89% rename from x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_active_instances.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_active_instances.spec.ts index 1b15e03c91987..b490482b4dd52 100644 --- a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_active_instances.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_active_instances.spec.ts @@ -6,15 +6,15 @@ */ import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import expect from '@kbn/expect'; import { sumBy } from 'lodash'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { config, expectedValues, generateData } from './generate_data'; +import { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -36,8 +36,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } - // FLAKY: https://github.com/elastic/kibana/issues/177639 - registry.when('Serverless active instances', { config: 'basic', archives: [] }, () => { + describe('Serverless active instances', () => { const { memoryTotal, billedDurationMs, @@ -47,8 +46,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { } = config; const { expectedMemoryUsed } = expectedValues; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await generateData({ start, end, apmSynthtraceEsClient }); }); diff --git a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_functions_overview.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_functions_overview.spec.ts similarity index 84% rename from x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_functions_overview.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_functions_overview.spec.ts index 94792228a2859..3acd0921d2602 100644 --- a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_functions_overview.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_functions_overview.spec.ts @@ -7,13 +7,13 @@ import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { config, expectedValues, generateData } from './generate_data'; +import { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -34,8 +34,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } - // FLAKY: https://github.com/elastic/kibana/issues/177641 - registry.when('Serverless functions overview', { config: 'basic', archives: [] }, () => { + describe('Serverless functions overview', () => { const { memoryTotal, billedDurationMs, @@ -45,7 +44,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { } = config; const { expectedMemoryUsed } = expectedValues; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await generateData({ start, end, apmSynthtraceEsClient }); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_metrics_charts.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_metrics_charts.spec.ts new file mode 100644 index 0000000000000..7f1e3c2a05004 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_metrics_charts.spec.ts @@ -0,0 +1,331 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { meanBy, sumBy } from 'lodash'; +import { Coordinate } from '@kbn/apm-plugin/typings/timeseries'; +import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; +import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import { generateData, config } from './generate_data'; +import { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; + +function isNotNullOrZeroCoordinate(coordinate: Coordinate) { + return coordinate.y !== null && coordinate.y !== 0; +} + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + const numberOfTransactionsCreated = 15; + + async function callApi(serviceName: string, serverlessId?: string) { + return await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/metrics/serverless/charts', + params: { + path: { serviceName }, + query: { + environment: 'test', + kuery: '', + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + bucketSizeInSeconds: 60, + ...(serverlessId ? { serverlessId } : {}), + }, + }, + }); + } + + describe('Serverless metrics charts', () => { + describe('when data is not loaded', () => { + let serverlessMetrics: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/charts'>; + before(async () => { + const response = await callApi('lambda-python'); + serverlessMetrics = response.body; + }); + + it('returns empty', () => { + serverlessMetrics.charts.forEach((chart) => { + expect(chart.series).to.be.empty(); + }); + }); + }); + + describe('when data is loaded', () => { + const { + memoryTotal, + memoryFree, + billedDurationMs, + coldStartDurationPython, + transactionDuration, + pythonServerlessFunctionNames, + serverlessId, + } = config; + + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await generateData({ start, end, apmSynthtraceEsClient }); + }); + + after(() => apmSynthtraceEsClient.clean()); + + describe('Python service', () => { + let serverlessMetrics: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/charts'>; + before(async () => { + const response = await callApi('lambda-python'); + serverlessMetrics = response.body; + }); + + it('returns all metrics chart', () => { + expect(serverlessMetrics.charts.length).to.be.greaterThan(0); + expect(serverlessMetrics.charts.map(({ title }) => title).sort()).to.eql([ + 'Cold start duration', + 'Cold starts', + 'Compute usage', + 'Lambda Duration', + 'System memory usage', + ]); + }); + + describe('Avg. Duration', () => { + const transactionDurationInMicroSeconds = transactionDuration * 1000; + [ + { title: 'Billed Duration', expectedValue: billedDurationMs * 1000 }, + { title: 'Transaction Duration', expectedValue: transactionDurationInMicroSeconds }, + ].map(({ title, expectedValue }) => + it(`returns correct ${title} value`, () => { + const avgDurationMetric = serverlessMetrics.charts.find((chart) => { + return chart.key === 'avg_duration'; + }); + const series = avgDurationMetric?.series.find((item) => item.title === title); + expect(series?.overallValue).to.eql(expectedValue); + const meanValue = meanBy(series?.data.filter(isNotNullOrZeroCoordinate), 'y'); + expect(meanValue).to.eql(expectedValue); + }) + ); + }); + + let metricsChart: (typeof serverlessMetrics.charts)[0] | undefined; + + describe('Cold start duration', () => { + before(() => { + metricsChart = serverlessMetrics.charts.find((chart) => { + return chart.key === 'cold_start_duration'; + }); + }); + it('returns correct overall value', () => { + expect(metricsChart?.series[0].overallValue).to.equal(coldStartDurationPython * 1000); + }); + + it('returns correct mean value', () => { + const meanValue = meanBy( + metricsChart?.series[0]?.data.filter(isNotNullOrZeroCoordinate), + 'y' + ); + expect(meanValue).to.equal(coldStartDurationPython * 1000); + }); + }); + + describe('Cold start count', () => { + before(() => { + metricsChart = serverlessMetrics.charts.find((chart) => { + return chart.key === 'cold_start_count'; + }); + }); + + it('returns correct overall value', () => { + expect(metricsChart?.series[0].overallValue).to.equal( + numberOfTransactionsCreated * pythonServerlessFunctionNames.length + ); + }); + + it('returns correct sum value', () => { + const sumValue = sumBy( + metricsChart?.series[0]?.data.filter(isNotNullOrZeroCoordinate), + 'y' + ); + expect(sumValue).to.equal( + numberOfTransactionsCreated * pythonServerlessFunctionNames.length + ); + }); + }); + + describe('memory usage', () => { + const expectedFreeMemory = 1 - memoryFree / memoryTotal; + [ + { title: 'Max', expectedValue: expectedFreeMemory }, + { title: 'Average', expectedValue: expectedFreeMemory }, + ].map(({ title, expectedValue }) => + it(`returns correct ${title} value`, () => { + const memoryUsageMetric = serverlessMetrics.charts.find((chart) => { + return chart.key === 'memory_usage_chart'; + }); + const series = memoryUsageMetric?.series.find((item) => item.title === title); + expect(series?.overallValue).to.eql(expectedValue); + const meanValue = meanBy(series?.data.filter(isNotNullOrZeroCoordinate), 'y'); + expect(meanValue).to.eql(expectedValue); + }) + ); + }); + + describe('Compute usage', () => { + const GBSeconds = 1024 * 1024 * 1024 * 1000; + let computeUsageMetric: (typeof serverlessMetrics.charts)[0] | undefined; + before(() => { + computeUsageMetric = serverlessMetrics.charts.find((chart) => { + return chart.key === 'compute_usage'; + }); + }); + it('returns correct overall value', () => { + const expectedValue = + ((memoryTotal * billedDurationMs) / GBSeconds) * numberOfTransactionsCreated * 2; + expect(computeUsageMetric?.series[0].overallValue).to.equal(expectedValue); + }); + + it('returns correct mean value', () => { + const expectedValue = ((memoryTotal * billedDurationMs) / GBSeconds) * 2; + const meanValue = meanBy( + computeUsageMetric?.series[0]?.data.filter((item) => item.y !== 0), + 'y' + ); + expect(meanValue).to.equal(expectedValue); + }); + }); + }); + + describe('detailed metrics', () => { + let serverlessMetrics: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/charts'>; + before(async () => { + const response = await callApi( + 'lambda-python', + `${serverlessId}${pythonServerlessFunctionNames[0]}` + ); + serverlessMetrics = response.body; + }); + + it('returns all metrics chart', () => { + expect(serverlessMetrics.charts.length).to.be.greaterThan(0); + expect(serverlessMetrics.charts.map(({ title }) => title).sort()).to.eql([ + 'Cold start duration', + 'Cold starts', + 'Compute usage', + 'Lambda Duration', + 'System memory usage', + ]); + }); + + describe('Avg. Duration', () => { + const transactionDurationInMicroSeconds = transactionDuration * 1000; + [ + { title: 'Billed Duration', expectedValue: billedDurationMs * 1000 }, + { title: 'Transaction Duration', expectedValue: transactionDurationInMicroSeconds }, + ].map(({ title, expectedValue }) => + it(`returns correct ${title} value`, () => { + const avgDurationMetric = serverlessMetrics.charts.find((chart) => { + return chart.key === 'avg_duration'; + }); + const series = avgDurationMetric?.series.find((item) => item.title === title); + expect(series?.overallValue).to.eql(expectedValue); + const meanValue = meanBy(series?.data.filter(isNotNullOrZeroCoordinate), 'y'); + expect(meanValue).to.eql(expectedValue); + }) + ); + }); + + let metricsChart: (typeof serverlessMetrics.charts)[0] | undefined; + + describe('Cold start duration', () => { + before(() => { + metricsChart = serverlessMetrics.charts.find((chart) => { + return chart.key === 'cold_start_duration'; + }); + }); + it('returns correct overall value', () => { + expect(metricsChart?.series[0].overallValue).to.equal(coldStartDurationPython * 1000); + }); + + it('returns correct mean value', () => { + const meanValue = meanBy( + metricsChart?.series[0]?.data.filter(isNotNullOrZeroCoordinate), + 'y' + ); + expect(meanValue).to.equal(coldStartDurationPython * 1000); + }); + }); + + describe('Cold start count', () => { + before(() => { + metricsChart = serverlessMetrics.charts.find((chart) => { + return chart.key === 'cold_start_count'; + }); + }); + + it('returns correct overall value', () => { + expect(metricsChart?.series[0].overallValue).to.equal(numberOfTransactionsCreated); + }); + + it('returns correct sum value', () => { + const sumValue = sumBy( + metricsChart?.series[0]?.data.filter(isNotNullOrZeroCoordinate), + 'y' + ); + expect(sumValue).to.equal(numberOfTransactionsCreated); + }); + }); + + describe('memory usage', () => { + const expectedFreeMemory = 1 - memoryFree / memoryTotal; + [ + { title: 'Max', expectedValue: expectedFreeMemory }, + { title: 'Average', expectedValue: expectedFreeMemory }, + ].map(({ title, expectedValue }) => + it(`returns correct ${title} value`, () => { + const memoryUsageMetric = serverlessMetrics.charts.find((chart) => { + return chart.key === 'memory_usage_chart'; + }); + const series = memoryUsageMetric?.series.find((item) => item.title === title); + expect(series?.overallValue).to.eql(expectedValue); + const meanValue = meanBy(series?.data.filter(isNotNullOrZeroCoordinate), 'y'); + expect(meanValue).to.eql(expectedValue); + }) + ); + }); + + describe('Compute usage', () => { + const GBSeconds = 1024 * 1024 * 1024 * 1000; + let computeUsageMetric: (typeof serverlessMetrics.charts)[0] | undefined; + before(() => { + computeUsageMetric = serverlessMetrics.charts.find((chart) => { + return chart.key === 'compute_usage'; + }); + }); + it('returns correct overall value', () => { + const expectedValue = + ((memoryTotal * billedDurationMs) / GBSeconds) * numberOfTransactionsCreated; + expect(computeUsageMetric?.series[0].overallValue).to.equal(expectedValue); + }); + + it('returns correct mean value', () => { + const expectedValue = (memoryTotal * billedDurationMs) / GBSeconds; + const meanValue = meanBy( + computeUsageMetric?.series[0]?.data.filter((item) => item.y !== 0), + 'y' + ); + expect(meanValue).to.equal(expectedValue); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_summary.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_summary.spec.ts new file mode 100644 index 0000000000000..c291ffab47648 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_summary.spec.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import expect from '@kbn/expect'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import { config, expectedValues, generateData } from './generate_data'; +import { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + async function callApi(serviceName: string, serverlessId?: string) { + return await apmApiClient.readUser({ + endpoint: `GET /internal/apm/services/{serviceName}/metrics/serverless/summary`, + params: { + path: { serviceName }, + query: { + environment: 'test', + kuery: '', + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + ...(serverlessId ? { serverlessId } : {}), + }, + }, + }); + } + + describe('Serverless overview', () => { + describe('when data is not loaded', () => { + let serverlessSummary: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/summary'>; + before(async () => { + const response = await callApi('lambda-python'); + serverlessSummary = response.body; + }); + + it('returns empty', () => { + expect(serverlessSummary).to.be.empty(); + }); + }); + + describe('when data is loaded', () => { + const { billedDurationMs, pythonServerlessFunctionNames, faasDuration, serverlessId } = + config; + const { expectedMemoryUsedRate } = expectedValues; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await generateData({ start, end, apmSynthtraceEsClient }); + }); + + after(() => apmSynthtraceEsClient.clean()); + + describe('Python service', () => { + let serverlessSummary: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/summary'>; + before(async () => { + const response = await callApi('lambda-python'); + serverlessSummary = response.body; + }); + + it('returns correct memory avg', () => { + expect(serverlessSummary.memoryUsageAvgRate).to.eql(expectedMemoryUsedRate); + }); + it('returns correct serverless function total', () => { + expect(serverlessSummary.serverlessFunctionsTotal).to.eql( + pythonServerlessFunctionNames.length + ); + }); + it('returns correct serverless duration avg', () => { + expect(serverlessSummary.serverlessDurationAvg).to.eql(faasDuration); + }); + it('returns correct billed duration avg', () => { + expect(serverlessSummary.billedDurationAvg).to.eql(billedDurationMs); + }); + }); + + describe('detailed metrics', () => { + let serverlessSummary: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/summary'>; + before(async () => { + const response = await callApi( + 'lambda-python', + `${serverlessId}${pythonServerlessFunctionNames[0]}` + ); + serverlessSummary = response.body; + }); + + it('returns correct memory avg', () => { + expect(serverlessSummary.memoryUsageAvgRate).to.eql(expectedMemoryUsedRate); + }); + it('returns correct serverless function total', () => { + expect(serverlessSummary.serverlessFunctionsTotal).to.eql(1); + }); + it('returns correct serverless duration avg', () => { + expect(serverlessSummary.serverlessDurationAvg).to.eql(faasDuration); + }); + it('returns correct billed duration avg', () => { + expect(serverlessSummary.billedDurationAvg).to.eql(billedDurationMs); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/mobile/generate_mobile_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/mobile/generate_mobile_data.ts index a4420b3f53c7c..fa3323c411d61 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/mobile/generate_mobile_data.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/mobile/generate_mobile_data.ts @@ -5,7 +5,7 @@ * 2.0. */ import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; export const SERVICE_VERSIONS = ['2.3', '1.2', '1.1']; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/mobile/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/mobile/index.ts index 97d8e13256d60..d54eb187b29c6 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/mobile/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/mobile/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('Mobile', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/observability_overview/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/observability_overview/index.ts index c43e15d005bb9..bbb35976f8175 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/observability_overview/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/observability_overview/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('observability_overview', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/observability_overview/observability_overview.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/observability_overview/observability_overview.spec.ts index 740dd432b670b..796ef08f70250 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/observability_overview/observability_overview.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/observability_overview/observability_overview.spec.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { meanBy, sumBy } from 'lodash'; import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { roundNumber } from '../../../../../../apm_api_integration/utils'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/index.ts index e88208d48a9b5..458361077c208 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('service_groups', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/save_service_group.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/save_service_group.spec.ts index a0ed02739cf9f..0b4db96e880e3 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/save_service_group.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/save_service_group.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ import expect from '@kbn/expect'; -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { ApmApiError } from '../../../../../../apm_api_integration/common/apm_api_supertest'; import { expectToReject } from '../../../../../../apm_api_integration/common/utils/expect_to_reject'; import { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_count/service_group_count.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_count/service_group_count.spec.ts index 21ac03197a422..cbb29e2729dcb 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_count/service_group_count.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_count/service_group_count.spec.ts @@ -5,8 +5,8 @@ * 2.0. */ import expect from '@kbn/expect'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; import { createServiceGroupApi, deleteAllServiceGroups, diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_with_overflow/service_group_with_overflow.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_with_overflow/service_group_with_overflow.spec.ts index 9324dee60d4e7..b5cbf1ae2566c 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_with_overflow/service_group_with_overflow.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_with_overflow/service_group_with_overflow.spec.ts @@ -9,8 +9,8 @@ import { ValuesType } from 'utility-types'; import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values'; import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; import { createServiceGroupApi, deleteAllServiceGroups } from '../service_groups_api_methods'; import { createServiceTransactionMetricsDocs } from './es_utils'; import { generateData } from './generate_data'; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_nodes/get_service_nodes.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_nodes/get_service_nodes.spec.ts new file mode 100644 index 0000000000000..95f0b8defa989 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_nodes/get_service_nodes.spec.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { apm, timerange } from '@kbn/apm-synthtrace-client'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + const serviceName = 'synth-go'; + const instanceName = 'instance-a'; + + async function callApi() { + return await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/metrics/nodes', + params: { + path: { serviceName }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + kuery: '', + environment: 'ENVIRONMENT_ALL', + }, + }, + }); + } + + describe('Service nodes', () => { + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await callApi(); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatchInline(` + Object { + "serviceNodes": Array [], + } + `); + }); + }); + + describe('when data is loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + const instance = apm + .service({ name: serviceName, environment: 'production', agentName: 'go' }) + .instance(instanceName); + await apmSynthtraceEsClient.index( + timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => + instance + .appMetrics({ + 'system.process.cpu.total.norm.pct': 1, + 'jvm.memory.heap.used': 1000, + 'jvm.memory.non_heap.used': 100, + 'jvm.thread.count': 25, + }) + .timestamp(timestamp) + ) + ); + }); + after(() => apmSynthtraceEsClient.clean()); + + it('returns service nodes', async () => { + const response = await callApi(); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatchInline(` + Object { + "serviceNodes": Array [ + Object { + "cpu": 1, + "heapMemory": 1000, + "hostName": "instance-a", + "name": "instance-a", + "nonHeapMemory": 100, + "threadCount": 25, + }, + ], + } + `); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_nodes/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_nodes/index.ts new file mode 100644 index 0000000000000..18315aade3b31 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_nodes/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('service_nodes', () => { + loadTestFile(require.resolve('./get_service_nodes.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/common/fixtures/es_archiver/metrics_8.0.0/mappings.json b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/metrics_8.0.0/mappings.json index 5cc5a0032cd31..b1044d198a9a3 100644 --- a/x-pack/test/apm_api_integration/common/fixtures/es_archiver/metrics_8.0.0/mappings.json +++ b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/metrics_8.0.0/mappings.json @@ -4073,10 +4073,6 @@ "index": { "auto_expand_replicas": "0-1", "codec": "best_compression", - "lifecycle": { - "name": "apm-rollover-30-days", - "rollover_alias": "apm-8.0.0-metric" - }, "mapping": { "total_fields": { "limit": "2000" @@ -4085,8 +4081,7 @@ "max_docvalue_fields_search": "200", "number_of_replicas": "0", "number_of_shards": "1", - "priority": "100", - "refresh_interval": "1ms" + "priority": "100" } } } diff --git a/x-pack/test/apm_api_integration/tests/diagnostics/index_pattern_settings.ts b/x-pack/test/apm_api_integration/tests/diagnostics/index_pattern_settings.ts deleted file mode 100644 index d0ba7b1850d31..0000000000000 --- a/x-pack/test/apm_api_integration/tests/diagnostics/index_pattern_settings.ts +++ /dev/null @@ -1,106 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import { getApmIndexTemplateNames } from '@kbn/apm-plugin/server/routes/diagnostics/helpers/get_apm_index_template_names'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const es = getService('es'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - const synthtraceKibanaClient = getService('synthtraceKibanaClient'); - - const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - - registry.when('Diagnostics: Index pattern settings', { config: 'basic', archives: [] }, () => { - describe('When there is no data', () => { - before(async () => { - // delete APM index templates - await es.indices.deleteIndexTemplate({ - name: Object.values(getApmIndexTemplateNames()).flat(), - }); - }); - - it('returns the built-in (non-APM) index templates`', async () => { - const { status, body } = await apmApiClient.adminUser({ - endpoint: 'GET /internal/apm/diagnostics', - }); - expect(status).to.be(200); - - const templateNames = body.indexTemplatesByIndexPattern.flatMap(({ indexTemplates }) => { - return indexTemplates?.map(({ templateName }) => templateName); - }); - - expect(templateNames).to.eql(['logs', 'metrics']); - }); - }); - - describe('When data is ingested', () => { - before(async () => { - const latestVersion = await synthtraceKibanaClient.fetchLatestApmPackageVersion(); - await synthtraceKibanaClient.installApmPackage(latestVersion); - - const instance = apm - .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) - .instance('instance-a'); - await apmSynthtraceEsClient.index( - timerange(start, end) - .interval('1m') - .rate(30) - .generator((timestamp) => - instance - .transaction({ transactionName: 'GET /users' }) - .timestamp(timestamp) - .duration(100) - .success() - ) - ); - }); - - after(() => apmSynthtraceEsClient.clean()); - - it('returns APM index templates', async () => { - const { status, body } = await apmApiClient.adminUser({ - endpoint: 'GET /internal/apm/diagnostics', - }); - expect(status).to.be(200); - - const templateNames = body.indexTemplatesByIndexPattern.flatMap(({ indexTemplates }) => { - return indexTemplates?.map(({ templateName }) => templateName); - }); - - expect(templateNames).to.eql([ - 'logs-apm.error', - 'logs-apm.app', - 'logs', - 'metrics-apm.service_transaction.60m', - 'metrics-apm.service_destination.10m', - 'metrics-apm.transaction.1m', - 'metrics-apm.service_destination.1m', - 'metrics-apm.service_transaction.10m', - 'metrics-apm.service_transaction.1m', - 'metrics-apm.transaction.60m', - 'metrics-apm.service_destination.60m', - 'metrics-apm.service_summary.1m', - 'metrics-apm.transaction.10m', - 'metrics-apm.internal', - 'metrics-apm.service_summary.10m', - 'metrics-apm.service_summary.60m', - 'metrics-apm.app', - 'metrics', - 'traces-apm', - 'traces-apm.rum', - 'traces-apm.sampled', - ]); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/metrics/metrics_charts.spec.ts b/x-pack/test/apm_api_integration/tests/metrics/metrics_charts.spec.ts deleted file mode 100644 index 92e7c8a80bcb1..0000000000000 --- a/x-pack/test/apm_api_integration/tests/metrics/metrics_charts.spec.ts +++ /dev/null @@ -1,462 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { first } from 'lodash'; -import { GenericMetricsChart } from '@kbn/apm-plugin/server/routes/metrics/fetch_and_transform_metrics'; -import { SupertestReturnType } from '../../common/apm_api_supertest'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -type ChartResponse = SupertestReturnType<'GET /internal/apm/services/{serviceName}/metrics/charts'>; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - - registry.when( - 'Metrics charts when data is loaded', - { config: 'basic', archives: ['metrics_8.0.0'] }, - () => { - describe('for opbeans-node', () => { - describe('returns metrics data', () => { - let chartsResponse: ChartResponse; - before(async () => { - chartsResponse = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/metrics/charts', - params: { - path: { serviceName: 'opbeans-node' }, - query: { - start: '2020-09-08T14:50:00.000Z', - end: '2020-09-08T14:55:00.000Z', - agentName: 'nodejs', - environment: 'ENVIRONMENT_ALL', - kuery: ``, - }, - }, - }); - }); - - it('contains CPU usage and System memory usage chart data', async () => { - expect(chartsResponse.status).to.be(200); - expectSnapshot(chartsResponse.body.charts.map((chart) => chart.title)).toMatchInline(` - Array [ - "CPU usage", - "System memory usage", - ] - `); - }); - - describe('CPU usage', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'cpu_usage_chart' - ); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "System max", - "System average", - "Process max", - "Process average", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0.714, - 0.3877, - 0.75, - 0.2543, - ] - `); - }); - }); - - describe("System memory usage (using 'system.memory' fields to calculate the memory usage)", () => { - let systemMemoryUsageChart: GenericMetricsChart | undefined; - before(() => { - systemMemoryUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'memory_usage_chart' - ); - }); - - it('has correct series', () => { - expect(systemMemoryUsageChart).to.not.empty(); - expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)) - .toMatchInline(` - Array [ - "Max", - "Average", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0.722093920925555, - 0.718173546796348, - ] - `); - }); - }); - }); - }); - - describe('for opbeans-java', () => { - describe('returns metrics data', () => { - let chartsResponse: ChartResponse; - before(async () => { - chartsResponse = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/metrics/charts', - params: { - path: { serviceName: 'opbeans-java' }, - query: { - start: '2020-09-08T14:55:30.000Z', - end: '2020-09-08T15:00:00.000Z', - agentName: 'java', - environment: 'ENVIRONMENT_ALL', - kuery: ``, - }, - }, - }); - }); - - it('has correct chart data', async () => { - expect(chartsResponse.status).to.be(200); - expectSnapshot(chartsResponse.body.charts.map((chart) => chart.title)).toMatchInline(` - Array [ - "CPU usage", - "System memory usage", - "Heap Memory", - "Non-Heap Memory", - "Thread Count", - "Garbage collection per minute", - "Garbage collection time spent per minute", - ] - `); - }); - - describe('CPU usage', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'cpu_usage_chart' - ); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "System max", - "System average", - "Process max", - "Process average", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0.203, - 0.178777777777778, - 0.01, - 0.009, - ] - `); - }); - - it('has the correct rate', async () => { - const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); - expectSnapshot(yValues).toMatchInline(` - Array [ - 0.193, - 0.193, - 0.009, - 0.009, - ] - `); - }); - }); - - describe("System memory usage (using 'system.process.cgroup' fields to calculate the memory usage)", () => { - let systemMemoryUsageChart: GenericMetricsChart | undefined; - before(() => { - systemMemoryUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'memory_usage_chart' - ); - }); - - it('has correct series', () => { - expect(systemMemoryUsageChart).to.not.empty(); - expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)) - .toMatchInline(` - Array [ - "Max", - "Average", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0.707924703557837, - 0.705395980841182, - ] - `); - }); - - it('has the correct rate', async () => { - const yValues = systemMemoryUsageChart?.series.map((serie) => first(serie.data)?.y); - expectSnapshot(yValues).toMatchInline(` - Array [ - 0.707924703557837, - 0.707924703557837, - ] - `); - }); - }); - - describe('Heap Memory', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'heap_memory_area_chart' - ); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "Avg. used", - "Avg. committed", - "Avg. limit", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 222501617.777778, - 374341632, - 1560281088, - ] - `); - }); - - it('has the correct rate', async () => { - const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); - expectSnapshot(yValues).toMatchInline(` - Array [ - 211472896, - 374341632, - 1560281088, - ] - `); - }); - }); - - describe('Non-Heap Memory', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'non_heap_memory_area_chart' - ); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "Avg. used", - "Avg. committed", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 138573397.333333, - 147677639.111111, - ] - `); - }); - - it('has the correct rate', async () => { - const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); - expectSnapshot(yValues).toMatchInline(` - Array [ - 138162752, - 147386368, - ] - `); - }); - }); - - describe('Thread Count', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'thread_count_line_chart' - ); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "Avg. count", - "Max count", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 44.4444444444444, - 45, - ] - `); - }); - - it('has the correct rate', async () => { - const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); - expectSnapshot(yValues).toMatchInline(` - Array [ - 44, - 44, - ] - `); - }); - }); - - describe('Garbage collection per minute', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'gc_rate_line_chart' - ); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "G1 Old Generation", - "G1 Young Generation", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0, - 3, - ] - `); - }); - }); - - describe('Garbage collection time spent per minute', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'gc_time_line_chart' - ); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "G1 Old Generation", - "G1 Young Generation", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0, - 37500, - ] - `); - }); - }); - }); - - // 9223372036854771712 = memory limit for a c-group when no memory limit is specified - it('calculates system memory usage using system total field when cgroup limit is equal to 9223372036854771712', async () => { - const chartsResponse = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/metrics/charts', - params: { - path: { serviceName: 'opbeans-java' }, - query: { - start: '2020-09-08T15:00:30.000Z', - end: '2020-09-08T15:05:00.000Z', - agentName: 'java', - environment: 'ENVIRONMENT_ALL', - kuery: ``, - }, - }, - }); - - const systemMemoryUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'memory_usage_chart' - ); - - expect(systemMemoryUsageChart).to.not.empty(); - expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "Max", - "Average", - ] - `); - expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0.114523896426499, - 0.114002376090415, - ] - `); - - const yValues = systemMemoryUsageChart?.series.map((serie) => first(serie.data)?.y); - expectSnapshot(yValues).toMatchInline(` - Array [ - 0.11383724014064, - 0.11383724014064, - ] - `); - }); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_metrics_charts.spec.ts b/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_metrics_charts.spec.ts deleted file mode 100644 index be823d8cc9898..0000000000000 --- a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_metrics_charts.spec.ts +++ /dev/null @@ -1,333 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { meanBy, sumBy } from 'lodash'; -import { Coordinate } from '@kbn/apm-plugin/typings/timeseries'; -import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; -import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { generateData, config } from './generate_data'; - -function isNotNullOrZeroCoordinate(coordinate: Coordinate) { - return coordinate.y !== null && coordinate.y !== 0; -} - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - const numberOfTransactionsCreated = 15; - - async function callApi(serviceName: string, serverlessId?: string) { - return await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/metrics/serverless/charts', - params: { - path: { serviceName }, - query: { - environment: 'test', - kuery: '', - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - bucketSizeInSeconds: 60, - ...(serverlessId ? { serverlessId } : {}), - }, - }, - }); - } - - registry.when( - 'Serverless metrics charts when data is not loaded', - { config: 'basic', archives: [] }, - () => { - let serverlessMetrics: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/charts'>; - before(async () => { - const response = await callApi('lambda-python'); - serverlessMetrics = response.body; - }); - - it('returns empty', () => { - serverlessMetrics.charts.forEach((chart) => { - expect(chart.series).to.be.empty(); - }); - }); - } - ); - - // FLAKY: https://github.com/elastic/kibana/issues/177642 - registry.when('Serverless metrics charts', { config: 'basic', archives: [] }, () => { - const { - memoryTotal, - memoryFree, - billedDurationMs, - coldStartDurationPython, - transactionDuration, - pythonServerlessFunctionNames, - serverlessId, - } = config; - - // eslint-disable-next-line mocha/no-sibling-hooks - before(async () => { - await generateData({ start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - describe('Python service', () => { - let serverlessMetrics: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/charts'>; - before(async () => { - const response = await callApi('lambda-python'); - serverlessMetrics = response.body; - }); - - it('returns all metrics chart', () => { - expect(serverlessMetrics.charts.length).to.be.greaterThan(0); - expect(serverlessMetrics.charts.map(({ title }) => title).sort()).to.eql([ - 'Cold start duration', - 'Cold starts', - 'Compute usage', - 'Lambda Duration', - 'System memory usage', - ]); - }); - - describe('Avg. Duration', () => { - const transactionDurationInMicroSeconds = transactionDuration * 1000; - [ - { title: 'Billed Duration', expectedValue: billedDurationMs * 1000 }, - { title: 'Transaction Duration', expectedValue: transactionDurationInMicroSeconds }, - ].map(({ title, expectedValue }) => - it(`returns correct ${title} value`, () => { - const avgDurationMetric = serverlessMetrics.charts.find((chart) => { - return chart.key === 'avg_duration'; - }); - const series = avgDurationMetric?.series.find((item) => item.title === title); - expect(series?.overallValue).to.eql(expectedValue); - const meanValue = meanBy(series?.data.filter(isNotNullOrZeroCoordinate), 'y'); - expect(meanValue).to.eql(expectedValue); - }) - ); - }); - - let metricsChart: (typeof serverlessMetrics.charts)[0] | undefined; - - describe('Cold start duration', () => { - before(() => { - metricsChart = serverlessMetrics.charts.find((chart) => { - return chart.key === 'cold_start_duration'; - }); - }); - it('returns correct overall value', () => { - expect(metricsChart?.series[0].overallValue).to.equal(coldStartDurationPython * 1000); - }); - - it('returns correct mean value', () => { - const meanValue = meanBy( - metricsChart?.series[0]?.data.filter(isNotNullOrZeroCoordinate), - 'y' - ); - expect(meanValue).to.equal(coldStartDurationPython * 1000); - }); - }); - - describe('Cold start count', () => { - before(() => { - metricsChart = serverlessMetrics.charts.find((chart) => { - return chart.key === 'cold_start_count'; - }); - }); - - it('returns correct overall value', () => { - expect(metricsChart?.series[0].overallValue).to.equal( - numberOfTransactionsCreated * pythonServerlessFunctionNames.length - ); - }); - - it('returns correct sum value', () => { - const sumValue = sumBy( - metricsChart?.series[0]?.data.filter(isNotNullOrZeroCoordinate), - 'y' - ); - expect(sumValue).to.equal( - numberOfTransactionsCreated * pythonServerlessFunctionNames.length - ); - }); - }); - - describe('memory usage', () => { - const expectedFreeMemory = 1 - memoryFree / memoryTotal; - [ - { title: 'Max', expectedValue: expectedFreeMemory }, - { title: 'Average', expectedValue: expectedFreeMemory }, - ].map(({ title, expectedValue }) => - it(`returns correct ${title} value`, () => { - const memoryUsageMetric = serverlessMetrics.charts.find((chart) => { - return chart.key === 'memory_usage_chart'; - }); - const series = memoryUsageMetric?.series.find((item) => item.title === title); - expect(series?.overallValue).to.eql(expectedValue); - const meanValue = meanBy(series?.data.filter(isNotNullOrZeroCoordinate), 'y'); - expect(meanValue).to.eql(expectedValue); - }) - ); - }); - - describe('Compute usage', () => { - const GBSeconds = 1024 * 1024 * 1024 * 1000; - let computeUsageMetric: (typeof serverlessMetrics.charts)[0] | undefined; - before(() => { - computeUsageMetric = serverlessMetrics.charts.find((chart) => { - return chart.key === 'compute_usage'; - }); - }); - it('returns correct overall value', () => { - const expectedValue = - ((memoryTotal * billedDurationMs) / GBSeconds) * numberOfTransactionsCreated * 2; - expect(computeUsageMetric?.series[0].overallValue).to.equal(expectedValue); - }); - - it('returns correct mean value', () => { - const expectedValue = ((memoryTotal * billedDurationMs) / GBSeconds) * 2; - const meanValue = meanBy( - computeUsageMetric?.series[0]?.data.filter((item) => item.y !== 0), - 'y' - ); - expect(meanValue).to.equal(expectedValue); - }); - }); - }); - - describe('detailed metrics', () => { - let serverlessMetrics: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/charts'>; - before(async () => { - const response = await callApi( - 'lambda-python', - `${serverlessId}${pythonServerlessFunctionNames[0]}` - ); - serverlessMetrics = response.body; - }); - - it('returns all metrics chart', () => { - expect(serverlessMetrics.charts.length).to.be.greaterThan(0); - expect(serverlessMetrics.charts.map(({ title }) => title).sort()).to.eql([ - 'Cold start duration', - 'Cold starts', - 'Compute usage', - 'Lambda Duration', - 'System memory usage', - ]); - }); - - describe('Avg. Duration', () => { - const transactionDurationInMicroSeconds = transactionDuration * 1000; - [ - { title: 'Billed Duration', expectedValue: billedDurationMs * 1000 }, - { title: 'Transaction Duration', expectedValue: transactionDurationInMicroSeconds }, - ].map(({ title, expectedValue }) => - it(`returns correct ${title} value`, () => { - const avgDurationMetric = serverlessMetrics.charts.find((chart) => { - return chart.key === 'avg_duration'; - }); - const series = avgDurationMetric?.series.find((item) => item.title === title); - expect(series?.overallValue).to.eql(expectedValue); - const meanValue = meanBy(series?.data.filter(isNotNullOrZeroCoordinate), 'y'); - expect(meanValue).to.eql(expectedValue); - }) - ); - }); - - let metricsChart: (typeof serverlessMetrics.charts)[0] | undefined; - - describe('Cold start duration', () => { - before(() => { - metricsChart = serverlessMetrics.charts.find((chart) => { - return chart.key === 'cold_start_duration'; - }); - }); - it('returns correct overall value', () => { - expect(metricsChart?.series[0].overallValue).to.equal(coldStartDurationPython * 1000); - }); - - it('returns correct mean value', () => { - const meanValue = meanBy( - metricsChart?.series[0]?.data.filter(isNotNullOrZeroCoordinate), - 'y' - ); - expect(meanValue).to.equal(coldStartDurationPython * 1000); - }); - }); - - describe('Cold start count', () => { - before(() => { - metricsChart = serverlessMetrics.charts.find((chart) => { - return chart.key === 'cold_start_count'; - }); - }); - - it('returns correct overall value', () => { - expect(metricsChart?.series[0].overallValue).to.equal(numberOfTransactionsCreated); - }); - - it('returns correct sum value', () => { - const sumValue = sumBy( - metricsChart?.series[0]?.data.filter(isNotNullOrZeroCoordinate), - 'y' - ); - expect(sumValue).to.equal(numberOfTransactionsCreated); - }); - }); - - describe('memory usage', () => { - const expectedFreeMemory = 1 - memoryFree / memoryTotal; - [ - { title: 'Max', expectedValue: expectedFreeMemory }, - { title: 'Average', expectedValue: expectedFreeMemory }, - ].map(({ title, expectedValue }) => - it(`returns correct ${title} value`, () => { - const memoryUsageMetric = serverlessMetrics.charts.find((chart) => { - return chart.key === 'memory_usage_chart'; - }); - const series = memoryUsageMetric?.series.find((item) => item.title === title); - expect(series?.overallValue).to.eql(expectedValue); - const meanValue = meanBy(series?.data.filter(isNotNullOrZeroCoordinate), 'y'); - expect(meanValue).to.eql(expectedValue); - }) - ); - }); - - describe('Compute usage', () => { - const GBSeconds = 1024 * 1024 * 1024 * 1000; - let computeUsageMetric: (typeof serverlessMetrics.charts)[0] | undefined; - before(() => { - computeUsageMetric = serverlessMetrics.charts.find((chart) => { - return chart.key === 'compute_usage'; - }); - }); - it('returns correct overall value', () => { - const expectedValue = - ((memoryTotal * billedDurationMs) / GBSeconds) * numberOfTransactionsCreated; - expect(computeUsageMetric?.series[0].overallValue).to.equal(expectedValue); - }); - - it('returns correct mean value', () => { - const expectedValue = (memoryTotal * billedDurationMs) / GBSeconds; - const meanValue = meanBy( - computeUsageMetric?.series[0]?.data.filter((item) => item.y !== 0), - 'y' - ); - expect(meanValue).to.equal(expectedValue); - }); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_summary.spec.ts b/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_summary.spec.ts deleted file mode 100644 index 065597eed1709..0000000000000 --- a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_summary.spec.ts +++ /dev/null @@ -1,111 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { config, expectedValues, generateData } from './generate_data'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - async function callApi(serviceName: string, serverlessId?: string) { - return await apmApiClient.readUser({ - endpoint: `GET /internal/apm/services/{serviceName}/metrics/serverless/summary`, - params: { - path: { serviceName }, - query: { - environment: 'test', - kuery: '', - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - ...(serverlessId ? { serverlessId } : {}), - }, - }, - }); - } - - registry.when( - 'Serverless overview when data is not loaded', - { config: 'basic', archives: [] }, - () => { - let serverlessSummary: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/summary'>; - before(async () => { - const response = await callApi('lambda-python'); - serverlessSummary = response.body; - }); - - it('returns empty', () => { - expect(serverlessSummary).to.be.empty(); - }); - } - ); - - // FLAKY: https://github.com/elastic/kibana/issues/177650 - registry.when('Serverless overview', { config: 'basic', archives: [] }, () => { - const { billedDurationMs, pythonServerlessFunctionNames, faasDuration, serverlessId } = config; - const { expectedMemoryUsedRate } = expectedValues; - - // eslint-disable-next-line mocha/no-sibling-hooks - before(async () => { - await generateData({ start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - describe('Python service', () => { - let serverlessSummary: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/summary'>; - before(async () => { - const response = await callApi('lambda-python'); - serverlessSummary = response.body; - }); - - it('returns correct memory avg', () => { - expect(serverlessSummary.memoryUsageAvgRate).to.eql(expectedMemoryUsedRate); - }); - it('returns correct serverless function total', () => { - expect(serverlessSummary.serverlessFunctionsTotal).to.eql( - pythonServerlessFunctionNames.length - ); - }); - it('returns correct serverless duration avg', () => { - expect(serverlessSummary.serverlessDurationAvg).to.eql(faasDuration); - }); - it('returns correct billed duration avg', () => { - expect(serverlessSummary.billedDurationAvg).to.eql(billedDurationMs); - }); - }); - - describe('detailed metrics', () => { - let serverlessSummary: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/summary'>; - before(async () => { - const response = await callApi( - 'lambda-python', - `${serverlessId}${pythonServerlessFunctionNames[0]}` - ); - serverlessSummary = response.body; - }); - - it('returns correct memory avg', () => { - expect(serverlessSummary.memoryUsageAvgRate).to.eql(expectedMemoryUsedRate); - }); - it('returns correct serverless function total', () => { - expect(serverlessSummary.serverlessFunctionsTotal).to.eql(1); - }); - it('returns correct serverless duration avg', () => { - expect(serverlessSummary.serverlessDurationAvg).to.eql(faasDuration); - }); - it('returns correct billed duration avg', () => { - expect(serverlessSummary.billedDurationAvg).to.eql(billedDurationMs); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/service_nodes/get_service_nodes.spec.ts b/x-pack/test/apm_api_integration/tests/service_nodes/get_service_nodes.spec.ts deleted file mode 100644 index 6b24587b6bc13..0000000000000 --- a/x-pack/test/apm_api_integration/tests/service_nodes/get_service_nodes.spec.ts +++ /dev/null @@ -1,96 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiClient = getService('apmApiClient'); - const registry = getService('registry'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - const serviceName = 'synth-go'; - const instanceName = 'instance-a'; - - async function callApi() { - return await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/metrics/nodes', - params: { - path: { serviceName }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - kuery: '', - environment: 'ENVIRONMENT_ALL', - }, - }, - }); - } - - registry.when('Service nodes when data is not loaded', { config: 'basic', archives: [] }, () => { - it('handles the empty state', async () => { - const response = await callApi(); - - expect(response.status).to.be(200); - - expectSnapshot(response.body).toMatchInline(` - Object { - "serviceNodes": Array [], - } - `); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177496 - registry.when('Service nodes when data is loaded', { config: 'basic', archives: [] }, () => { - before(async () => { - const instance = apm - .service({ name: serviceName, environment: 'production', agentName: 'go' }) - .instance(instanceName); - await apmSynthtraceEsClient.index( - timerange(start, end) - .interval('1m') - .rate(1) - .generator((timestamp) => - instance - .appMetrics({ - 'system.process.cpu.total.norm.pct': 1, - 'jvm.memory.heap.used': 1000, - 'jvm.memory.non_heap.used': 100, - 'jvm.thread.count': 25, - }) - .timestamp(timestamp) - ) - ); - }); - after(() => apmSynthtraceEsClient.clean()); - - it('returns service nodes', async () => { - const response = await callApi(); - - expect(response.status).to.be(200); - - expectSnapshot(response.body).toMatchInline(` - Object { - "serviceNodes": Array [ - Object { - "cpu": 1, - "heapMemory": 1000, - "hostName": "instance-a", - "name": "instance-a", - "nonHeapMemory": 100, - "threadCount": 25, - }, - ], - } - `); - }); - }); -} diff --git a/x-pack/test/cases_api_integration/common/lib/api/attachments.ts b/x-pack/test/cases_api_integration/common/lib/api/attachments.ts index 9c06f9b3efa08..7147a1fdf03ee 100644 --- a/x-pack/test/cases_api_integration/common/lib/api/attachments.ts +++ b/x-pack/test/cases_api_integration/common/lib/api/attachments.ts @@ -18,6 +18,7 @@ import { BulkCreateAttachmentsRequest, AttachmentPatchRequest, AttachmentsFindResponse, + PostFileAttachmentRequest, } from '@kbn/cases-plugin/common/types/api'; import { Attachments, Attachment } from '@kbn/cases-plugin/common/types/domain'; import { User } from '../authentication/types'; @@ -79,6 +80,33 @@ export const createComment = async ({ return theCase; }; +export const createFileAttachment = async ({ + supertest, + caseId, + params, + auth = { user: superUser, space: null }, + expectedHttpCode = 200, + headers = {}, +}: { + supertest: SuperTest.Agent; + caseId: string; + params: PostFileAttachmentRequest; + auth?: { user: User; space: string | null } | null; + expectedHttpCode?: number; + headers?: Record; +}): Promise => { + const apiCall = supertest.post(`${getSpaceUrlPrefix(auth?.space)}${CASES_URL}/${caseId}/files`); + void setupAuth({ apiCall, headers, auth }); + + const { body: theCase } = await apiCall + .set('kbn-xsrf', 'true') + .set(headers) + .attach('file', Buffer.from(params.file as unknown as string), params.filename) + .expect(expectedHttpCode); + + return theCase; +}; + export const bulkCreateAttachments = async ({ supertest, caseId, diff --git a/x-pack/test/cases_api_integration/common/lib/mock.ts b/x-pack/test/cases_api_integration/common/lib/mock.ts index 969df10a53d1d..2461cded5aeaa 100644 --- a/x-pack/test/cases_api_integration/common/lib/mock.ts +++ b/x-pack/test/cases_api_integration/common/lib/mock.ts @@ -20,7 +20,10 @@ import { PersistableStateAttachmentPayload, Attachment, } from '@kbn/cases-plugin/common/types/domain'; -import type { CasePostRequest } from '@kbn/cases-plugin/common/types/api'; +import type { + CasePostRequest, + PostFileAttachmentRequest, +} from '@kbn/cases-plugin/common/types/api'; import { FILE_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common/constants'; import { ConnectorTypes } from '@kbn/cases-plugin/common/types/domain'; import { FILE_SO_TYPE } from '@kbn/files-plugin/common'; @@ -64,6 +67,11 @@ export const postCommentUserReq: UserCommentAttachmentPayload = { owner: 'securitySolutionFixture', }; +export const postFileReq: PostFileAttachmentRequest = { + file: 'This is a file, a buffer will be created from this string.', + filename: 'foobar.txt', +}; + export const postCommentAlertReq: AlertAttachmentPayload = { alertId: 'test-id', index: 'test-index', diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts index fbfa4354683a5..d38af59f57d16 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts @@ -79,6 +79,9 @@ export default ({ getService }: FtrProviderContext): void => { describe('delete_cases', () => { afterEach(async () => { + await deleteAllFiles({ + supertest, + }); await deleteAllCaseItems(es); }); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/files/post_file.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/files/post_file.ts new file mode 100644 index 0000000000000..b5e27bd85e172 --- /dev/null +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/files/post_file.ts @@ -0,0 +1,179 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { ExternalReferenceAttachmentAttributes } from '@kbn/cases-plugin/common/types/domain'; +import { SECURITY_SOLUTION_OWNER } from '@kbn/cases-plugin/common'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { defaultUser, postCaseReq, postFileReq } from '../../../../common/lib/mock'; +import { + createCase, + createFileAttachment, + removeServerGeneratedPropertiesFromSavedObject, + getAuthWithSuperUser, + deleteAllCaseItems, + superUserSpace1Auth, + deleteAllFiles, +} from '../../../../common/lib/api'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + const caseRequest = { ...postCaseReq, owner: SECURITY_SOLUTION_OWNER }; + + describe('post_file', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + describe('happy path', () => { + afterEach(async () => { + await deleteAllFiles({ + supertest, + auth: authSpace1, + }); + }); + + it('should post a file in space1', async () => { + const postedCase = await createCase(supertest, caseRequest, 200, authSpace1); + const patchedCase = await createFileAttachment({ + supertest, + caseId: postedCase.id, + params: postFileReq, + auth: authSpace1, + }); + + const attachedFileComment = removeServerGeneratedPropertiesFromSavedObject( + patchedCase.comments![0] + ) as ExternalReferenceAttachmentAttributes; + // @ts-ignore + const fileMetadata = attachedFileComment.externalReferenceMetadata?.files[0]; + + expect(attachedFileComment.owner).to.be('securitySolution'); + expect(attachedFileComment.externalReferenceId).to.be.ok(); // truthy + expect(attachedFileComment.externalReferenceAttachmentTypeId).to.be('.files'); + expect(attachedFileComment.externalReferenceStorage).to.eql({ + soType: 'file', + type: 'savedObject', + }); + expect(fileMetadata.name).to.be('foobar'); + expect(fileMetadata.mimeType).to.be('text/plain'); + expect(fileMetadata.extension).to.be('txt'); + + // updates the case correctly after adding a comment + expect(patchedCase.totalComment).to.eql(patchedCase.comments!.length); + expect(patchedCase.updated_by).to.eql(defaultUser); + }); + }); + + describe('unhappy path', () => { + it('should return a 400 when posting a file without a filename', async () => { + const postedCase = await createCase(supertest, caseRequest, 200, authSpace1); + await createFileAttachment({ + supertest, + caseId: postedCase.id, + params: { ...postFileReq, filename: '' }, + auth: authSpace1, + expectedHttpCode: 400, + }); + }); + + it('should return a 400 when posting a file with a filename that is too long', async () => { + const longFilename = Array(161).fill('a').toString(); + + const postedCase = await createCase(supertest, caseRequest, 200, authSpace1); + await createFileAttachment({ + supertest, + caseId: postedCase.id, + params: { ...postFileReq, filename: longFilename }, + auth: authSpace1, + expectedHttpCode: 400, + }); + }); + + it('should return a 400 when posting a file with an invalid mime type', async () => { + const postedCase = await createCase(supertest, caseRequest, 200, authSpace1); + await createFileAttachment({ + supertest, + caseId: postedCase.id, + params: { ...postFileReq, filename: 'foobar.124zas' }, + auth: authSpace1, + expectedHttpCode: 400, + }); + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should not post a file when the user does not have permissions for that owner', async () => { + const postedCase = await createCase(supertest, caseRequest, 200, authSpace1); + + await createFileAttachment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postFileReq, + auth: { user: obsOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should not post a file`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + caseRequest, + 200, + superUserSpace1Auth + ); + + await createFileAttachment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postFileReq, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should not post a file in a space the user does not have permissions for', async () => { + const postedCase = await createCase(supertestWithoutAuth, caseRequest, 200, { + user: superUser, + space: 'space2', + }); + + await createFileAttachment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postFileReq, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); + }); +}; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts index f9360e473080d..6378ce936e9ea 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts @@ -21,6 +21,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./comments/get_all_comments')); loadTestFile(require.resolve('./comments/patch_comment')); loadTestFile(require.resolve('./comments/post_comment')); + loadTestFile(require.resolve('./files/post_file')); loadTestFile(require.resolve('./alerts/get_cases')); loadTestFile(require.resolve('./alerts/get_alerts_attached_to_case')); loadTestFile(require.resolve('./cases/delete_cases')); diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/files/post_file.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/files/post_file.ts new file mode 100644 index 0000000000000..8e624620b0d4e --- /dev/null +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/files/post_file.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { ExternalReferenceAttachmentAttributes } from '@kbn/cases-plugin/common/types/domain'; +import { SECURITY_SOLUTION_OWNER } from '@kbn/cases-plugin/common'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { nullUser, postCaseReq, postFileReq } from '../../../../common/lib/mock'; +import { + createCase, + createFileAttachment, + removeServerGeneratedPropertiesFromSavedObject, + getAuthWithSuperUser, + deleteAllCaseItems, + deleteAllFiles, +} from '../../../../common/lib/api'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + // const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('post_file', () => { + afterEach(async () => { + await deleteAllFiles({ + supertest: supertestWithoutAuth, + auth: authSpace1, + }); + await deleteAllCaseItems(es); + }); + + it('should post a file in space1', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + { ...postCaseReq, owner: SECURITY_SOLUTION_OWNER }, + 200, + authSpace1 + ); + const patchedCase = await createFileAttachment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postFileReq, + auth: authSpace1, + }); + + const attachedFileComment = removeServerGeneratedPropertiesFromSavedObject( + patchedCase.comments![0] + ) as ExternalReferenceAttachmentAttributes; + // @ts-ignore + const fileMetadata = attachedFileComment.externalReferenceMetadata?.files[0]; + + expect(attachedFileComment.owner).to.be('securitySolution'); + expect(attachedFileComment.externalReferenceId).to.be.ok(); // truthy + expect(attachedFileComment.externalReferenceAttachmentTypeId).to.be('.files'); + expect(attachedFileComment.externalReferenceStorage).to.eql({ + soType: 'file', + type: 'savedObject', + }); + expect(fileMetadata.name).to.be('foobar'); + expect(fileMetadata.mimeType).to.be('text/plain'); + expect(fileMetadata.extension).to.be('txt'); + + // updates the case correctly after adding a comment + expect(patchedCase.totalComment).to.eql(patchedCase.comments!.length); + expect(patchedCase.updated_by).to.eql(nullUser); + }); + + it('should not post a file on a case in a different space', async () => { + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1); + await createFileAttachment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postFileReq, + auth: getAuthWithSuperUser('space2'), + expectedHttpCode: 404, + }); + }); + }); +}; diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/index.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/index.ts index 81da0454f48a4..c8de077b16bca 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/index.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/index.ts @@ -17,6 +17,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./comments/get_all_comments')); loadTestFile(require.resolve('./comments/patch_comment')); loadTestFile(require.resolve('./comments/post_comment')); + loadTestFile(require.resolve('./files/post_file')); loadTestFile(require.resolve('./cases/delete_cases')); loadTestFile(require.resolve('./cases/find_cases')); loadTestFile(require.resolve('./cases/get_case')); diff --git a/x-pack/test/functional/apps/index_management/index_template_wizard.ts b/x-pack/test/functional/apps/index_management/index_template_wizard.ts index 581a0b2761644..d932b96d4f6a1 100644 --- a/x-pack/test/functional/apps/index_management/index_template_wizard.ts +++ b/x-pack/test/functional/apps/index_management/index_template_wizard.ts @@ -67,6 +67,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const stepTitle = await testSubjects.getVisibleText('stepTitle'); expect(stepTitle).to.be('Index settings (optional)'); + // Verify that index mode callout is displayed + const indexModeCalloutText = await testSubjects.getVisibleText('indexModeCallout'); + expect(indexModeCalloutText).to.be( + 'The index.mode setting has been set to Standard within template Logistics. Any changes to index.mode set on this page will be overwritten by the Logistics selection.' + ); + // Click Next button await pageObjects.indexManagement.clickNextButton(); }); diff --git a/x-pack/test/observability_ai_assistant_api_integration/configs/index.ts b/x-pack/test/observability_ai_assistant_api_integration/configs/index.ts index 4a39c31b0a3a7..75f7bb628b4be 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/configs/index.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/configs/index.ts @@ -8,7 +8,6 @@ import { mapValues } from 'lodash'; import path from 'path'; import { createTestConfig, CreateTestConfig } from '../common/config'; -import { SUPPORTED_TRAINED_MODELS } from '../../functional/services/ml/api'; export const observabilityAIAssistantDebugLogger = { name: 'plugins.observabilityAIAssistant', @@ -31,7 +30,6 @@ export const observabilityAIAssistantFtrConfigs = { __dirname, '../../../../test/analytics/plugins/analytics_ftr_helpers' ), - 'xpack.observabilityAIAssistant.modelId': SUPPORTED_TRAINED_MODELS.TINY_ELSER.name, // TODO: Remove }, }, }; diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_setup.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_setup.spec.ts index b8cacaaa58351..7903f4b53966a 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_setup.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_setup.spec.ts @@ -34,7 +34,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .expect(200); expect(res.body.service_settings.model_id).to.be('pt_tiny_elser'); - expect(res.body.inference_id).to.be('ai_assistant_kb_inference'); + expect(res.body.inference_id).to.be('obs_ai_assistant_kb_inference'); await deleteKnowledgeBaseModel(ml); await deleteInferenceEndpoint({ es }); diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts index 76ad2d06e344a..8c10a6128d302 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts @@ -77,7 +77,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(res.body.ready).to.be(false); expect(res.body.enabled).to.be(true); expect(res.body.errorMessage).to.include.string( - 'Inference endpoint not found [ai_assistant_kb_inference]' + 'Inference endpoint not found [obs_ai_assistant_kb_inference]' ); }); }); diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_user_instructions.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_user_instructions.spec.ts index 04791909340ef..6ea2b279fd386 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_user_instructions.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_user_instructions.spec.ts @@ -30,6 +30,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const es = getService('es'); const ml = getService('ml'); const log = getService('log'); + const retry = getService('retry'); const getScopedApiClientForUsername = getService('getScopedApiClientForUsername'); describe('Knowledge base user instructions', () => { @@ -94,62 +95,69 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('"editor" can retrieve their own private instructions and the public instruction', async () => { - const res = await observabilityAIAssistantAPIClient.editor({ - endpoint: 'GET /internal/observability_ai_assistant/kb/user_instructions', - }); + await retry.try(async () => { + const res = await observabilityAIAssistantAPIClient.editor({ + endpoint: 'GET /internal/observability_ai_assistant/kb/user_instructions', + }); - const instructions = res.body.userInstructions; + const instructions = res.body.userInstructions; + expect(instructions).to.have.length(3); - const sortById = (data: Array) => sortBy(data, 'id'); + const sortById = (data: Array) => sortBy(data, 'id'); - expect(sortById(instructions)).to.eql( - sortById([ - { - id: 'private-doc-from-editor', - public: false, - text: 'Private user instruction from "editor"', - }, - { - id: 'public-doc-from-editor', - public: true, - text: 'Public user instruction from "editor"', - }, - { - id: 'public-doc-from-secondary_editor', - public: true, - text: 'Public user instruction from "secondary_editor"', - }, - ]) - ); + expect(sortById(instructions)).to.eql( + sortById([ + { + id: 'private-doc-from-editor', + public: false, + text: 'Private user instruction from "editor"', + }, + { + id: 'public-doc-from-editor', + public: true, + text: 'Public user instruction from "editor"', + }, + { + id: 'public-doc-from-secondary_editor', + public: true, + text: 'Public user instruction from "secondary_editor"', + }, + ]) + ); + }); }); it('"secondaryEditor" can retrieve their own private instructions and the public instruction', async () => { - const res = await observabilityAIAssistantAPIClient.secondaryEditor({ - endpoint: 'GET /internal/observability_ai_assistant/kb/user_instructions', - }); - const instructions = res.body.userInstructions; + await retry.try(async () => { + const res = await observabilityAIAssistantAPIClient.secondaryEditor({ + endpoint: 'GET /internal/observability_ai_assistant/kb/user_instructions', + }); - const sortById = (data: Array) => sortBy(data, 'id'); + const instructions = res.body.userInstructions; + expect(instructions).to.have.length(3); - expect(sortById(instructions)).to.eql( - sortById([ - { - id: 'public-doc-from-editor', - public: true, - text: 'Public user instruction from "editor"', - }, - { - id: 'public-doc-from-secondary_editor', - public: true, - text: 'Public user instruction from "secondary_editor"', - }, - { - id: 'private-doc-from-secondary_editor', - public: false, - text: 'Private user instruction from "secondary_editor"', - }, - ]) - ); + const sortById = (data: Array) => sortBy(data, 'id'); + + expect(sortById(instructions)).to.eql( + sortById([ + { + id: 'public-doc-from-editor', + public: true, + text: 'Public user instruction from "editor"', + }, + { + id: 'public-doc-from-secondary_editor', + public: true, + text: 'Public user instruction from "secondary_editor"', + }, + { + id: 'private-doc-from-secondary_editor', + public: false, + text: 'Private user instruction from "secondary_editor"', + }, + ]) + ); + }); }); }); diff --git a/x-pack/test_serverless/api_integration/services/index_management/svl_templates.helpers.ts b/x-pack/test_serverless/api_integration/services/index_management/svl_templates.helpers.ts index 0f6006fd91470..8c96011936743 100644 --- a/x-pack/test_serverless/api_integration/services/index_management/svl_templates.helpers.ts +++ b/x-pack/test_serverless/api_integration/services/index_management/svl_templates.helpers.ts @@ -56,6 +56,7 @@ export function SvlTemplatesHelpers({ getService }: FtrProviderContext) { const baseTemplate: TemplateDeserialized = { name, indexPatterns, + indexMode: 'standard', version: 1, template: { ...getTemplateMock(isMappingsSourceFieldEnabled) }, _kbnMeta: { diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts b/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts index 1e0dff8bef632..1190a642a3518 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts @@ -90,6 +90,7 @@ export default function ({ getService }: FtrProviderContext) { const expectedKeys = [ 'name', 'indexPatterns', + 'indexMode', 'hasSettings', 'hasAliases', 'hasMappings', @@ -114,6 +115,7 @@ export default function ({ getService }: FtrProviderContext) { const expectedKeys = [ 'name', 'indexPatterns', + 'indexMode', 'template', '_kbnMeta', 'allowAutoCreate', diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/config.ts b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/config.ts index 14078f228c7c8..01e470d2a7d88 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/config.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/config.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { SUPPORTED_TRAINED_MODELS } from '@kbn/test-suites-xpack/functional/services/ml/api'; import { createTestConfig } from '../../../config.base'; import { ObservabilityAIAssistantServices } from './common/ftr_provider_context'; import { services as inheritedServices } from '../../../services'; @@ -28,7 +27,4 @@ export default createTestConfig({ // include settings from project controller // https://github.com/elastic/project-controller/blob/main/internal/project/observability/config/elasticsearch.yml esServerArgs: ['xpack.ml.dfa.enabled=false'], - kbnServerArgs: [ - `--xpack.observabilityAIAssistant.modelId=${SUPPORTED_TRAINED_MODELS.TINY_ELSER.name}`, // TODO: Remove - ], }); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_setup.spec.ts b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_setup.spec.ts index a792b01b0e2cb..eeef06464c9d6 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_setup.spec.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_setup.spec.ts @@ -56,7 +56,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .expect(200); expect(res.body.service_settings.model_id).to.be('pt_tiny_elser'); - expect(res.body.inference_id).to.be('ai_assistant_kb_inference'); + expect(res.body.inference_id).to.be('obs_ai_assistant_kb_inference'); await deleteKnowledgeBaseModel(ml); await deleteInferenceEndpoint({ es }); diff --git a/x-pack/test_serverless/functional/test_suites/search/elasticsearch_start.ts b/x-pack/test_serverless/functional/test_suites/search/elasticsearch_start.ts index 39228137cf7d7..ba370871c07ff 100644 --- a/x-pack/test_serverless/functional/test_suites/search/elasticsearch_start.ts +++ b/x-pack/test_serverless/functional/test_suites/search/elasticsearch_start.ts @@ -26,7 +26,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await esDeleteAllIndices(['search-*', 'test-*']); }; - describe('Elasticsearch Start [Onboarding Empty State]', function () { + // Failing: See https://github.com/elastic/kibana/issues/200020 + describe.skip('Elasticsearch Start [Onboarding Empty State]', function () { describe('developer', function () { before(async () => { await pageObjects.svlCommonPage.loginWithRole('developer'); diff --git a/yarn.lock b/yarn.lock index 2dd177d56de3c..b69c7755172e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1394,10 +1394,10 @@ resolved "https://registry.yarnpkg.com/@bazel/ibazel/-/ibazel-0.16.2.tgz#05dd7f06659759fda30f87b15534f1e42f1201bb" integrity sha512-KgqAWMH0emL6f3xH6nqyTryoBMqlJ627LBIe9PT1PRRQPz2FtHib3FIHJPukp1slzF3hJYZvdiVwgPnHbaSOOA== -"@bazel/runfiles@^5.8.1": - version "5.8.1" - resolved "https://registry.yarnpkg.com/@bazel/runfiles/-/runfiles-5.8.1.tgz#737d5b3dc9739767054820265cfe432a80564c82" - integrity sha512-NDdfpdQ6rZlylgv++iMn5FkObC/QlBQvipinGLSOguTYpRywmieOyJ29XHvUilspwTFSILWpoE9CqMGkHXug1g== +"@bazel/runfiles@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@bazel/runfiles/-/runfiles-6.3.1.tgz#3f8824b2d82853377799d42354b4df78ab0ace0b" + integrity sha512-1uLNT5NZsUVIGS4syuHwTzZ8HycMPyr6POA3FCE4GbMtc4rhoJk8aZKtNIRthJYfL+iioppi+rTfH3olMPr9nA== "@bazel/typescript@4.6.2": version "4.6.2" @@ -7447,25 +7447,25 @@ dependencies: ncp "^2.0.0" -"@launchdarkly/js-sdk-common@2.11.0": - version "2.11.0" - resolved "https://registry.yarnpkg.com/@launchdarkly/js-sdk-common/-/js-sdk-common-2.11.0.tgz#efc0c94ee4b11d72910c5bcdf9294154a27dbb1c" - integrity sha512-96Jg4QH347w2+rL4Bpykqw28+HHUAW4HapjIkIfM3giELK7BwXUp3BiAVxo2ax78e7A7KqvMzPJUx5r2EGpkMw== +"@launchdarkly/js-sdk-common@2.12.0": + version "2.12.0" + resolved "https://registry.yarnpkg.com/@launchdarkly/js-sdk-common/-/js-sdk-common-2.12.0.tgz#c22eb9fead687260d916a75f693c7d399f085b05" + integrity sha512-HIDxvgo1vksC9hsYy3517sgW0Ql+iW3fgwlq/CEigeBNmaa9/J1Pxo7LrKPzezEA0kaGedmt/DCzVVxVBmxSsQ== -"@launchdarkly/js-server-sdk-common@2.9.0": - version "2.9.0" - resolved "https://registry.yarnpkg.com/@launchdarkly/js-server-sdk-common/-/js-server-sdk-common-2.9.0.tgz#337d13d4bec596f6244b9723f7ec718ee087bfa5" - integrity sha512-hf/qkn+NvCkyoLl6fl+4Q737p4Jg3T+RnRqdkJDMxO+8aAI+vpXuD9bbhkgYA9XFHyQ9puhufRGzX1BX26b7Rg== +"@launchdarkly/js-server-sdk-common@2.9.1": + version "2.9.1" + resolved "https://registry.yarnpkg.com/@launchdarkly/js-server-sdk-common/-/js-server-sdk-common-2.9.1.tgz#a683d682897c20a6967f5454d932663e4da6fc5c" + integrity sha512-BGIjcfel1hURvX4hM4iVruWecWMntRzh1UuPtV0uOYnXLuETp5lfpqBB6KzFoERnEZoCyX6Tmo+tPFVwteIUGA== dependencies: - "@launchdarkly/js-sdk-common" "2.11.0" + "@launchdarkly/js-sdk-common" "2.12.0" semver "7.5.4" -"@launchdarkly/node-server-sdk@^9.7.0": - version "9.7.0" - resolved "https://registry.yarnpkg.com/@launchdarkly/node-server-sdk/-/node-server-sdk-9.7.0.tgz#87223c2d3ab5fc7186065a0974960c94f73573ad" - integrity sha512-ABOsjcjH9pFdyG1m5++lhP+ngxfx4GMcIfgTp0iSPncuh0dMxCCWSx831gbhxR9M+f2x4EnsQ9HEdwnmwktb9g== +"@launchdarkly/node-server-sdk@^9.7.1": + version "9.7.1" + resolved "https://registry.yarnpkg.com/@launchdarkly/node-server-sdk/-/node-server-sdk-9.7.1.tgz#bb228e5f8c4c7ca52579f909e4150b9a749b04a9" + integrity sha512-razZ/ine5hfHiS7ZNfM1r/G7GNDQ+wHV0pRN2A0Yk5spZTmT/ecu67e+aXu/KAfEQyL0V4ofEkXJpeGl7TYJ5Q== dependencies: - "@launchdarkly/js-server-sdk-common" "2.9.0" + "@launchdarkly/js-server-sdk-common" "2.9.1" https-proxy-agent "^5.0.1" launchdarkly-eventsource "2.0.3" @@ -11334,10 +11334,10 @@ resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.28.tgz#9ce8fa048c1e8c85cb71d7fe4d704e000226036f" integrity sha512-SMA+fUwULwK7sd/ZJicUztiPs8F1yCPwF3O23Z9uQ32ME5Ha0NmDK9+QTsYE4O2tHXChzXomSWWeIhCnoN1LqA== -"@types/selenium-webdriver@^4.1.26": - version "4.1.26" - resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.1.26.tgz#09c696a341cf8cfc1641cded11d14813350b6ca9" - integrity sha512-PUgqsyNffal0eAU0bzGlh37MJo558aporAPZoKqBeB/pF7zhKl1S3zqza0GpwFqgoigNxWhEIJzru75eeYco/w== +"@types/selenium-webdriver@^4.1.27": + version "4.1.27" + resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.1.27.tgz#e08000d649df6f099b4099432bd2fece9f50ea7b" + integrity sha512-ALqsj8D7Swb6MnBQuAQ58J3KC3yh6fLGtAmpBmnZX8j+0kmP7NaLt56CuzBw2W2bXPrvHFTgn8iekOQFUKXEQA== dependencies: "@types/node" "*" "@types/ws" "*" @@ -11767,10 +11767,10 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@wdio/logger@^9.0.0": - version "9.0.4" - resolved "https://registry.yarnpkg.com/@wdio/logger/-/logger-9.0.4.tgz#63e901b9f0f29fa1ded5af54006fbd4df2354c33" - integrity sha512-b6gcu0PTVb3fgK4kyAH/k5UUWN5FOUdAfhA4PAY/IZvxZTMFYMqnrZb0WRWWWqL6nu9pcrOVtCOdPBvj0cb+Nw== +"@wdio/logger@^9.1.3": + version "9.1.3" + resolved "https://registry.yarnpkg.com/@wdio/logger/-/logger-9.1.3.tgz#b64b3d2ac642498f3c97580e2f0971f13c1e8fbb" + integrity sha512-cumRMK/gE1uedBUw3WmWXOQ7HtB6DR8EyKQioUz2P0IJtRRpglMBdZV7Svr3b++WWawOuzZHMfbTkJQmaVt8Gw== dependencies: chalk "^5.1.2" loglevel "^1.6.0" @@ -12150,10 +12150,10 @@ resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== -"@zip.js/zip.js@^2.7.48": - version "2.7.51" - resolved "https://registry.yarnpkg.com/@zip.js/zip.js/-/zip.js-2.7.51.tgz#a434e285048b951a5788d3d2d59aa68f209e7141" - integrity sha512-RKHaebzZZgQkUuzb49/qweN69e8Np9AUZ9QygydDIrbG1njypSAKwkeqIVeuf2JVGBDyB7Z9HKvzPgYrSlv9gw== +"@zip.js/zip.js@^2.7.53": + version "2.7.53" + resolved "https://registry.yarnpkg.com/@zip.js/zip.js/-/zip.js-2.7.53.tgz#bf88e90d8eed562182c01339643bc405446b0578" + integrity sha512-G6Bl5wN9EXXVaTUIox71vIX5Z454zEBe+akKpV4m1tUboIctT5h7ID3QXCJd/Lfy2rSvmkTmZIucf1jGRR4f5A== a-sync-waterfall@^1.0.0: version "1.0.1" @@ -14218,10 +14218,10 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^130.0.1: - version "130.0.1" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-130.0.1.tgz#24fd5b2c9c9df4ebfc5d28c94eca8658915fbe15" - integrity sha512-JH+OxDZ7gVv02r9oXwj4mQ8JCtj62g0fCD1LMUUYdB/4mPxn/E2ys+1IzXItoE7vXM9fGVc9R1akvXLqwwuSww== +chromedriver@^130.0.4: + version "130.0.4" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-130.0.4.tgz#55225ddfec428e306116507651f5a24fdb299bd6" + integrity sha512-lpR+PWXszij1k4Ig3t338Zvll9HtCTiwoLM7n4pCCswALHxzmgwaaIFBh3rt9+5wRk9D07oFblrazrBxwaYYAQ== dependencies: "@testim/chrome-version" "^1.1.4" axios "^1.7.4" @@ -18603,19 +18603,19 @@ gcp-metadata@^6.1.0: gaxios "^6.0.0" json-bigint "^1.0.0" -geckodriver@^4.5.1: - version "4.5.1" - resolved "https://registry.yarnpkg.com/geckodriver/-/geckodriver-4.5.1.tgz#624fc01815c1aa498dd3f717f7bd4c6cca0c57b8" - integrity sha512-lGCRqPMuzbRNDWJOQcUqhNqPvNsIFu6yzXF8J/6K3WCYFd2r5ckbeF7h1cxsnjA7YLSEiWzERCt6/gjZ3tW0ug== +geckodriver@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/geckodriver/-/geckodriver-5.0.0.tgz#88437f3812075988bb05b5e19dc4aaa42d200577" + integrity sha512-vn7TtQ3b9VMJtVXsyWtQQl1fyBVFhQy7UvJF96kPuuJ0or5THH496AD3eUyaDD11+EqCxH9t6V+EP9soZQk4YQ== dependencies: - "@wdio/logger" "^9.0.0" - "@zip.js/zip.js" "^2.7.48" + "@wdio/logger" "^9.1.3" + "@zip.js/zip.js" "^2.7.53" decamelize "^6.0.0" http-proxy-agent "^7.0.2" https-proxy-agent "^7.0.5" node-fetch "^3.3.2" tar-fs "^3.0.6" - which "^4.0.0" + which "^5.0.0" gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2: version "1.0.0-beta.2" @@ -28129,12 +28129,12 @@ select-hose@^2.0.0: resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= -selenium-webdriver@^4.25.0: - version "4.25.0" - resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.25.0.tgz#3562b49668817974bb1d13d25a50e8bc0264fcf3" - integrity sha512-zl9IX93caOT8wbcCpZzAkEtYa+hNgJ4C5GUN8uhpzggqRLvsg1asfKi0p1uNZC8buYVvsBZbx8S+9MjVAjs4oA== +selenium-webdriver@^4.26.0: + version "4.26.0" + resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.26.0.tgz#23163cdad20388214a4ad17c1f38262a0857c902" + integrity sha512-nA7jMRIPV17mJmAiTDBWN96Sy0Uxrz5CCLb7bLVV6PpL417SyBMPc2Zo/uoREc2EOHlzHwHwAlFtgmSngSY4WQ== dependencies: - "@bazel/runfiles" "^5.8.1" + "@bazel/runfiles" "^6.3.1" jszip "^3.10.1" tmp "^0.2.3" ws "^8.18.0" @@ -32174,10 +32174,10 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -which@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/which/-/which-4.0.0.tgz#cd60b5e74503a3fbcfbf6cd6b4138a8bae644c1a" - integrity sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg== +which@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/which/-/which-5.0.0.tgz#d93f2d93f79834d4363c7d0c23e00d07c466c8d6" + integrity sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ== dependencies: isexe "^3.1.1"