diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index 812d22fec913bc..561f783e917ddd 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -45,8 +45,6 @@ disabled: - x-pack/plugins/observability/e2e/synthetics_run.ts # Configs that exist but weren't running in CI when this file was introduced - - test/visual_regression/config.ts - - x-pack/test/visual_regression/config.ts - x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/config.ts - x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/config.ts - x-pack/test/alerting_api_integration/spaces_only_legacy/config.ts @@ -142,6 +140,7 @@ enabled: - x-pack/test/detection_engine_api_integration/security_and_spaces/group7/config.ts - x-pack/test/detection_engine_api_integration/security_and_spaces/group8/config.ts - x-pack/test/detection_engine_api_integration/security_and_spaces/group9/config.ts + - x-pack/test/detection_engine_api_integration/security_and_spaces/group10/config.ts - x-pack/test/encrypted_saved_objects_api_integration/config.ts - x-pack/test/endpoint_api_integration_no_ingest/config.ts - x-pack/test/examples/config.ts @@ -183,7 +182,9 @@ enabled: - x-pack/test/functional/apps/maps/group2/config.ts - x-pack/test/functional/apps/maps/group3/config.ts - x-pack/test/functional/apps/maps/group4/config.ts - - x-pack/test/functional/apps/ml/anomaly_detection/config.ts + - x-pack/test/functional/apps/ml/anomaly_detection_jobs/config.ts + - x-pack/test/functional/apps/ml/anomaly_detection_integrations/config.ts + - x-pack/test/functional/apps/ml/anomaly_detection_result_views/config.ts - x-pack/test/functional/apps/ml/data_frame_analytics/config.ts - x-pack/test/functional/apps/ml/data_visualizer/config.ts - x-pack/test/functional/apps/ml/permissions/config.ts diff --git a/.eslintrc.js b/.eslintrc.js index 406e58e17d7a6e..da5dfb4eccb5d6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -593,7 +593,6 @@ module.exports = { 'test/*/config_open.ts', 'test/*/*.config.ts', 'test/*/{tests,test_suites,apis,apps}/**/*', - 'test/visual_regression/tests/**/*', 'x-pack/test/*/{tests,test_suites,apis,apps}/**/*', 'x-pack/test/*/*config.*ts', 'x-pack/test/saved_object_api_integration/*/apis/**/*', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9d53eb77959438..dbc10f3a6c41e2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,6 +13,7 @@ /src/plugins/saved_search/ @elastic/kibana-data-discovery /x-pack/plugins/discover_enhanced/ @elastic/kibana-data-discovery /test/functional/apps/discover/ @elastic/kibana-data-discovery +/test/functional/apps/context/ @elastic/kibana-data-discovery /test/api_integration/apis/unified_field_list/ @elastic/kibana-data-discovery /x-pack/plugins/graph/ @elastic/kibana-data-discovery /x-pack/test/functional/apps/graph @elastic/kibana-data-discovery @@ -96,6 +97,7 @@ x-pack/examples/files_example @elastic/kibana-app-services # Unified Observability - on hold due to team capacity shortage # For now, if you're changing these pages, get a review from someone who understand the changes # /x-pack/plugins/observability/public/context @elastic/unified-observability +# /x-pack/test/observability_functional @elastic/unified-observability # Home/Overview/Landing Pages /x-pack/plugins/observability/public/pages/home @elastic/observability-design @@ -109,6 +111,7 @@ x-pack/examples/files_example @elastic/kibana-app-services /x-pack/plugins/observability/public/pages/cases @elastic/actionable-observability /x-pack/plugins/observability/public/pages/rules @elastic/actionable-observability /x-pack/plugins/observability/public/pages/rule_details @elastic/actionable-observability +/x-pack/test/observability_functional @elastic/actionable-observability # Infra Monitoring /x-pack/plugins/infra/ @elastic/infra-monitoring-ui @@ -218,7 +221,6 @@ x-pack/examples/files_example @elastic/kibana-app-services /x-pack/test/api_integration/apis/maps/ @elastic/kibana-gis /x-pack/test/functional/apps/maps/ @elastic/kibana-gis /x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis -/x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis /x-pack/plugins/stack_alerts/server/alert_types/geo_containment @elastic/kibana-gis /x-pack/plugins/stack_alerts/public/alert_types/geo_containment @elastic/kibana-gis #CC# /x-pack/plugins/file_upload @elastic/kibana-gis @@ -381,10 +383,10 @@ x-pack/examples/files_example @elastic/kibana-app-services /x-pack/plugins/security_solution/common/search_strategy/timeline @elastic/security-threat-hunting-investigations /x-pack/plugins/security_solution/common/types/timeline @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/cypress/integration/timeline_templates @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/cypress/integration/timeline @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/cypress/integration/detection_alerts @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/cypress/integration/urls @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/cypress/e2e/timeline_templates @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/cypress/e2e/timeline @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/cypress/e2e/detection_alerts @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/cypress/e2e/urls @elastic/security-threat-hunting-investigations /x-pack/plugins/security_solution/public/common/components/alerts_viewer @elastic/security-threat-hunting-investigations /x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_action @elastic/security-threat-hunting-investigations @@ -405,18 +407,18 @@ x-pack/examples/files_example @elastic/kibana-app-services /x-pack/plugins/security_solution/common/search_strategy/security_solution/network @elastic/security-threat-hunting-explore /x-pack/plugins/security_solution/common/search_strategy/security_solution/user @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/cypress/integration/cases @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/cypress/integration/host_details @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/cypress/integration/hosts @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/cypress/integration/network @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/cypress/integration/overview @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/cypress/integration/pagination @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/cypress/integration/users @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/cypress/e2e/cases @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/cypress/e2e/host_details @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/cypress/e2e/hosts @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/cypress/e2e/network @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/cypress/e2e/overview @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/cypress/e2e/pagination @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/cypress/e2e/users @elastic/security-threat-hunting-explore /x-pack/plugins/security_solution/cypress/screens/hosts @elastic/security-threat-hunting-explore /x-pack/plugins/security_solution/cypress/screens/network @elastic/security-threat-hunting-explore /x-pack/plugins/security_solution/cypress/tasks/hosts @elastic/security-threat-hunting-explore /x-pack/plugins/security_solution/cypress/tasks/network @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/cypress/upgrade_integration/threat_hunting/cases @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/cypress/upgrade_e2e/threat_hunting/cases @elastic/security-threat-hunting-explore /x-pack/plugins/security_solution/public/common/components/charts @elastic/security-threat-hunting-explore /x-pack/plugins/security_solution/public/common/components/header_page @elastic/security-threat-hunting-explore @@ -461,7 +463,7 @@ x-pack/examples/files_example @elastic/kibana-app-services ## Security Solution sub teams - Detections and Response Rules -/x-pack/plugins/security_solution/cypress/integration/detection_rules @elastic/security-detections-response-rules +/x-pack/plugins/security_solution/cypress/e2e/detection_rules @elastic/security-detections-response-rules /x-pack/plugins/security_solution/public/detections/components/rules @elastic/security-detections-response-rules /x-pack/plugins/security_solution/public/detections/components/severity @elastic/security-detections-response-rules @@ -486,9 +488,9 @@ x-pack/examples/files_example @elastic/kibana-app-services ## Security Solution sub teams - Security Platform /x-pack/plugins/lists @elastic/security-solution-platform -/x-pack/plugins/security_solution/cypress/integration/data_sources @elastic/security-solution-platform -/x-pack/plugins/security_solution/cypress/integration/exceptions @elastic/security-solution-platform -/x-pack/plugins/security_solution/cypress/integration/value_lists @elastic/security-solution-platform +/x-pack/plugins/security_solution/cypress/e2e/data_sources @elastic/security-solution-platform +/x-pack/plugins/security_solution/cypress/e2e/exceptions @elastic/security-solution-platform +/x-pack/plugins/security_solution/cypress/e2e/value_lists @elastic/security-solution-platform /x-pack/plugins/security_solution/public/common/components/exceptions @elastic/security-solution-platform /x-pack/plugins/security_solution/public/exceptions @elastic/security-solution-platform @@ -538,8 +540,8 @@ x-pack/plugins/security_solution/server/usage/ @elastic/security-data-analytics x-pack/plugins/security_solution/server/lib/telemetry/ @elastic/security-data-analytics ## Security Solution sub teams - security-engineering-productivity -x-pack/plugins/security_solution/cypress/ccs_integration @elastic/security-engineering-productivity -x-pack/plugins/security_solution/cypress/upgrade_integration @elastic/security-engineering-productivity +x-pack/plugins/security_solution/cypress/ccs_e2e @elastic/security-engineering-productivity +x-pack/plugins/security_solution/cypress/upgrade_e2e @elastic/security-engineering-productivity x-pack/plugins/security_solution/cypress/README.md @elastic/security-engineering-productivity x-pack/test/security_solution_cypress @elastic/security-engineering-productivity @@ -569,7 +571,7 @@ x-pack/test/threat_intelligence_cypress @elastic/protections-experience # Security Solution onboarding tour /x-pack/plugins/security_solution/public/common/components/guided_onboarding @elastic/platform-onboarding -/x-pack/plugins/security_solution/cypress/integration/guided_onboarding @elastic/platform-onboarding +/x-pack/plugins/security_solution/cypress/e2e/guided_onboarding @elastic/platform-onboarding # Design (at the bottom for specificity of SASS files) **/*.scss @elastic/kibana-design @@ -656,6 +658,8 @@ packages/core/application/core-application-browser @elastic/kibana-core packages/core/application/core-application-browser-internal @elastic/kibana-core packages/core/application/core-application-browser-mocks @elastic/kibana-core packages/core/application/core-application-common @elastic/kibana-core +packages/core/apps/core-apps-browser-internal @elastic/kibana-core +packages/core/apps/core-apps-browser-mocks @elastic/kibana-core packages/core/base/core-base-browser-internal @elastic/kibana-core packages/core/base/core-base-browser-mocks @elastic/kibana-core packages/core/base/core-base-common @elastic/kibana-core diff --git a/api_docs/actions.mdx b/api_docs/actions.mdx index 7b77296a7abb9d..c36b4f01cbb42a 100644 --- a/api_docs/actions.mdx +++ b/api_docs/actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/actions title: "actions" image: https://source.unsplash.com/400x175/?github description: API docs for the actions plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'actions'] --- import actionsObj from './actions.devdocs.json'; diff --git a/api_docs/advanced_settings.mdx b/api_docs/advanced_settings.mdx index 8497e2ae833d52..f40b3a65b671fe 100644 --- a/api_docs/advanced_settings.mdx +++ b/api_docs/advanced_settings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/advancedSettings title: "advancedSettings" image: https://source.unsplash.com/400x175/?github description: API docs for the advancedSettings plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'advancedSettings'] --- import advancedSettingsObj from './advanced_settings.devdocs.json'; diff --git a/api_docs/aiops.mdx b/api_docs/aiops.mdx index 28fd735f6e461e..c34a07fe1e093c 100644 --- a/api_docs/aiops.mdx +++ b/api_docs/aiops.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/aiops title: "aiops" image: https://source.unsplash.com/400x175/?github description: API docs for the aiops plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'aiops'] --- import aiopsObj from './aiops.devdocs.json'; diff --git a/api_docs/alerting.devdocs.json b/api_docs/alerting.devdocs.json index 5eecc7808e3037..77be0a447f31fa 100644 --- a/api_docs/alerting.devdocs.json +++ b/api_docs/alerting.devdocs.json @@ -2810,6 +2810,16 @@ "section": "def-common.IExecutionLogResult", "text": "IExecutionLogResult" }, + ">; getGlobalExecutionLogWithAuth: ({ dateStart, dateEnd, filter, page, perPage, sort, }: ", + "GetGlobalExecutionLogParams", + ") => Promise<", + { + "pluginId": "alerting", + "scope": "common", + "docId": "kibAlertingPluginApi", + "section": "def-common.IExecutionLogResult", + "text": "IExecutionLogResult" + }, ">; getActionErrorLog: ({ id, dateStart, dateEnd, filter, page, perPage, sort, }: ", "GetActionErrorLogByIdParams", ") => Promise<", @@ -4128,6 +4138,28 @@ "path": "x-pack/plugins/alerting/common/execution_log_types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.IExecutionLog.rule_id", + "type": "string", + "tags": [], + "label": "rule_id", + "description": [], + "path": "x-pack/plugins/alerting/common/execution_log_types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.IExecutionLog.rule_name", + "type": "string", + "tags": [], + "label": "rule_name", + "description": [], + "path": "x-pack/plugins/alerting/common/execution_log_types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/alerting.mdx b/api_docs/alerting.mdx index d331897a74bc21..6ebee8e813c9c7 100644 --- a/api_docs/alerting.mdx +++ b/api_docs/alerting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/alerting title: "alerting" image: https://source.unsplash.com/400x175/?github description: API docs for the alerting plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'alerting'] --- import alertingObj from './alerting.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Response Ops](https://github.com/orgs/elastic/teams/response-ops) for q | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 368 | 0 | 359 | 21 | +| 370 | 0 | 361 | 22 | ## Client diff --git a/api_docs/apm.devdocs.json b/api_docs/apm.devdocs.json index efaf84a5b304a1..e7268af447666b 100644 --- a/api_docs/apm.devdocs.json +++ b/api_docs/apm.devdocs.json @@ -792,7 +792,7 @@ "label": "APIEndpoint", "description": [], "signature": [ - "\"POST /internal/apm/data_view/static\" | \"GET /internal/apm/data_view/title\" | \"GET /internal/apm/environments\" | \"GET /internal/apm/services/{serviceName}/errors/groups/main_statistics\" | \"GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name\" | \"POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/errors/{groupId}\" | \"GET /internal/apm/services/{serviceName}/errors/distribution\" | \"GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions\" | \"POST /internal/apm/latency/overall_distribution/transactions\" | \"GET /internal/apm/services/{serviceName}/metrics/charts\" | \"GET /internal/apm/observability_overview\" | \"GET /internal/apm/observability_overview/has_data\" | \"GET /internal/apm/service-map\" | \"GET /internal/apm/service-map/service/{serviceName}\" | \"GET /internal/apm/service-map/dependency\" | \"GET /internal/apm/services/{serviceName}/serviceNodes\" | \"GET /internal/apm/services\" | \"POST /internal/apm/services/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/metadata/details\" | \"GET /internal/apm/services/{serviceName}/metadata/icons\" | \"GET /internal/apm/services/{serviceName}/agent\" | \"GET /internal/apm/services/{serviceName}/transaction_types\" | \"GET /internal/apm/services/{serviceName}/node/{serviceNodeName}/metadata\" | \"GET /api/apm/services/{serviceName}/annotation/search\" | \"POST /api/apm/services/{serviceName}/annotation\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}\" | \"GET /internal/apm/services/{serviceName}/throughput\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/dependencies\" | \"GET /internal/apm/services/{serviceName}/dependencies/breakdown\" | \"GET /internal/apm/services/{serviceName}/profiling/timeline\" | \"GET /internal/apm/services/{serviceName}/profiling/statistics\" | \"GET /internal/apm/services/{serviceName}/anomaly_charts\" | \"GET /internal/apm/sorted_and_filtered_services\" | \"GET /internal/apm/service-groups\" | \"GET /internal/apm/service-group\" | \"POST /internal/apm/service-group\" | \"DELETE /internal/apm/service-group\" | \"GET /internal/apm/service-group/services\" | \"GET /internal/apm/suggestions\" | \"GET /internal/apm/traces/{traceId}\" | \"GET /internal/apm/traces\" | \"GET /internal/apm/traces/{traceId}/root_transaction\" | \"GET /internal/apm/transactions/{transactionId}\" | \"GET /internal/apm/traces/find\" | \"GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics\" | \"GET /internal/apm/services/{serviceName}/transactions/groups/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/latency\" | \"GET /internal/apm/services/{serviceName}/transactions/traces/samples\" | \"GET /internal/apm/services/{serviceName}/transaction/charts/breakdown\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/error_rate\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate_by_transaction_name\" | \"GET /internal/apm/alerts/chart_preview/transaction_error_rate\" | \"GET /internal/apm/alerts/chart_preview/transaction_duration\" | \"GET /internal/apm/alerts/chart_preview/transaction_error_count\" | \"GET /api/apm/settings/agent-configuration\" | \"GET /api/apm/settings/agent-configuration/view\" | \"DELETE /api/apm/settings/agent-configuration\" | \"PUT /api/apm/settings/agent-configuration\" | \"POST /api/apm/settings/agent-configuration/search\" | \"GET /api/apm/settings/agent-configuration/environments\" | \"GET /api/apm/settings/agent-configuration/agent_name\" | \"GET /internal/apm/settings/anomaly-detection/jobs\" | \"POST /internal/apm/settings/anomaly-detection/jobs\" | \"GET /internal/apm/settings/anomaly-detection/environments\" | \"POST /internal/apm/settings/anomaly-detection/update_to_v3\" | \"GET /internal/apm/settings/apm-index-settings\" | \"GET /internal/apm/settings/apm-indices\" | \"POST /internal/apm/settings/apm-indices/save\" | \"GET /internal/apm/settings/custom_links/transaction\" | \"GET /internal/apm/settings/custom_links\" | \"POST /internal/apm/settings/custom_links\" | \"PUT /internal/apm/settings/custom_links/{id}\" | \"DELETE /internal/apm/settings/custom_links/{id}\" | \"GET /api/apm/sourcemaps\" | \"POST /api/apm/sourcemaps\" | \"DELETE /api/apm/sourcemaps/{id}\" | \"GET /internal/apm/fleet/has_apm_policies\" | \"GET /internal/apm/fleet/agents\" | \"POST /api/apm/fleet/apm_server_schema\" | \"GET /internal/apm/fleet/apm_server_schema/unsupported\" | \"GET /internal/apm/fleet/migration_check\" | \"POST /internal/apm/fleet/cloud_apm_package_policy\" | \"GET /internal/apm/fleet/java_agent_versions\" | \"GET /internal/apm/dependencies/top_dependencies\" | \"GET /internal/apm/dependencies/upstream_services\" | \"GET /internal/apm/dependencies/metadata\" | \"GET /internal/apm/dependencies/charts/latency\" | \"GET /internal/apm/dependencies/charts/throughput\" | \"GET /internal/apm/dependencies/charts/error_rate\" | \"GET /internal/apm/dependencies/operations\" | \"GET /internal/apm/dependencies/charts/distribution\" | \"GET /internal/apm/dependencies/operations/spans\" | \"GET /internal/apm/correlations/field_candidates/transactions\" | \"POST /internal/apm/correlations/field_stats/transactions\" | \"GET /internal/apm/correlations/field_value_stats/transactions\" | \"POST /internal/apm/correlations/field_value_pairs/transactions\" | \"POST /internal/apm/correlations/significant_correlations/transactions\" | \"POST /internal/apm/correlations/p_values/transactions\" | \"GET /internal/apm/fallback_to_transactions\" | \"GET /internal/apm/has_data\" | \"GET /internal/apm/event_metadata/{processorEvent}/{id}\" | \"GET /internal/apm/agent_keys\" | \"GET /internal/apm/agent_keys/privileges\" | \"POST /internal/apm/api_key/invalidate\" | \"POST /api/apm/agent_keys\" | \"GET /internal/apm/storage_explorer\" | \"GET /internal/apm/services/{serviceName}/storage_details\" | \"GET /internal/apm/storage_chart\" | \"GET /internal/apm/traces/{traceId}/span_links/{spanId}/parents\" | \"GET /internal/apm/traces/{traceId}/span_links/{spanId}/children\" | \"GET /internal/apm/services/{serviceName}/infrastructure_attributes\" | \"GET /internal/apm/debug-telemetry\" | \"GET /internal/apm/time_range_metadata\"" + "\"POST /internal/apm/data_view/static\" | \"GET /internal/apm/data_view/title\" | \"GET /internal/apm/environments\" | \"GET /internal/apm/services/{serviceName}/errors/groups/main_statistics\" | \"GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name\" | \"POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/errors/{groupId}\" | \"GET /internal/apm/services/{serviceName}/errors/distribution\" | \"GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions\" | \"POST /internal/apm/latency/overall_distribution/transactions\" | \"GET /internal/apm/services/{serviceName}/metrics/charts\" | \"GET /internal/apm/observability_overview\" | \"GET /internal/apm/observability_overview/has_data\" | \"GET /internal/apm/service-map\" | \"GET /internal/apm/service-map/service/{serviceName}\" | \"GET /internal/apm/service-map/dependency\" | \"GET /internal/apm/services/{serviceName}/serviceNodes\" | \"GET /internal/apm/services\" | \"POST /internal/apm/services/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/metadata/details\" | \"GET /internal/apm/services/{serviceName}/metadata/icons\" | \"GET /internal/apm/services/{serviceName}/agent\" | \"GET /internal/apm/services/{serviceName}/transaction_types\" | \"GET /internal/apm/services/{serviceName}/node/{serviceNodeName}/metadata\" | \"GET /api/apm/services/{serviceName}/annotation/search\" | \"POST /api/apm/services/{serviceName}/annotation\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}\" | \"GET /internal/apm/services/{serviceName}/throughput\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/dependencies\" | \"GET /internal/apm/services/{serviceName}/dependencies/breakdown\" | \"GET /internal/apm/services/{serviceName}/profiling/timeline\" | \"GET /internal/apm/services/{serviceName}/profiling/statistics\" | \"GET /internal/apm/services/{serviceName}/anomaly_charts\" | \"GET /internal/apm/sorted_and_filtered_services\" | \"GET /internal/apm/service-groups\" | \"GET /internal/apm/service-group\" | \"POST /internal/apm/service-group\" | \"DELETE /internal/apm/service-group\" | \"GET /internal/apm/service-group/services\" | \"GET /internal/apm/suggestions\" | \"GET /internal/apm/traces/{traceId}\" | \"GET /internal/apm/traces\" | \"GET /internal/apm/traces/{traceId}/root_transaction\" | \"GET /internal/apm/transactions/{transactionId}\" | \"GET /internal/apm/traces/find\" | \"GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics\" | \"GET /internal/apm/services/{serviceName}/transactions/groups/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/latency\" | \"GET /internal/apm/services/{serviceName}/transactions/traces/samples\" | \"GET /internal/apm/services/{serviceName}/transaction/charts/breakdown\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/error_rate\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate_by_transaction_name\" | \"GET /internal/apm/alerts/chart_preview/transaction_error_rate\" | \"GET /internal/apm/alerts/chart_preview/transaction_duration\" | \"GET /internal/apm/alerts/chart_preview/transaction_error_count\" | \"GET /api/apm/settings/agent-configuration\" | \"GET /api/apm/settings/agent-configuration/view\" | \"DELETE /api/apm/settings/agent-configuration\" | \"PUT /api/apm/settings/agent-configuration\" | \"POST /api/apm/settings/agent-configuration/search\" | \"GET /api/apm/settings/agent-configuration/environments\" | \"GET /api/apm/settings/agent-configuration/agent_name\" | \"GET /internal/apm/settings/anomaly-detection/jobs\" | \"POST /internal/apm/settings/anomaly-detection/jobs\" | \"GET /internal/apm/settings/anomaly-detection/environments\" | \"POST /internal/apm/settings/anomaly-detection/update_to_v3\" | \"GET /internal/apm/settings/apm-index-settings\" | \"GET /internal/apm/settings/apm-indices\" | \"POST /internal/apm/settings/apm-indices/save\" | \"GET /internal/apm/settings/custom_links/transaction\" | \"GET /internal/apm/settings/custom_links\" | \"POST /internal/apm/settings/custom_links\" | \"PUT /internal/apm/settings/custom_links/{id}\" | \"DELETE /internal/apm/settings/custom_links/{id}\" | \"GET /api/apm/sourcemaps\" | \"POST /api/apm/sourcemaps\" | \"DELETE /api/apm/sourcemaps/{id}\" | \"GET /internal/apm/fleet/has_apm_policies\" | \"GET /internal/apm/fleet/agents\" | \"POST /api/apm/fleet/apm_server_schema\" | \"GET /internal/apm/fleet/apm_server_schema/unsupported\" | \"GET /internal/apm/fleet/migration_check\" | \"POST /internal/apm/fleet/cloud_apm_package_policy\" | \"GET /internal/apm/fleet/java_agent_versions\" | \"GET /internal/apm/dependencies/top_dependencies\" | \"GET /internal/apm/dependencies/upstream_services\" | \"GET /internal/apm/dependencies/metadata\" | \"GET /internal/apm/dependencies/charts/latency\" | \"GET /internal/apm/dependencies/charts/throughput\" | \"GET /internal/apm/dependencies/charts/error_rate\" | \"GET /internal/apm/dependencies/operations\" | \"GET /internal/apm/dependencies/charts/distribution\" | \"GET /internal/apm/dependencies/operations/spans\" | \"GET /internal/apm/correlations/field_candidates/transactions\" | \"POST /internal/apm/correlations/field_stats/transactions\" | \"GET /internal/apm/correlations/field_value_stats/transactions\" | \"POST /internal/apm/correlations/field_value_pairs/transactions\" | \"POST /internal/apm/correlations/significant_correlations/transactions\" | \"POST /internal/apm/correlations/p_values/transactions\" | \"GET /internal/apm/fallback_to_transactions\" | \"GET /internal/apm/has_data\" | \"GET /internal/apm/event_metadata/{processorEvent}/{id}\" | \"GET /internal/apm/agent_keys\" | \"GET /internal/apm/agent_keys/privileges\" | \"POST /internal/apm/api_key/invalidate\" | \"POST /api/apm/agent_keys\" | \"GET /internal/apm/storage_explorer\" | \"GET /internal/apm/services/{serviceName}/storage_details\" | \"GET /internal/apm/storage_chart\" | \"GET /internal/apm/storage_explorer/privileges\" | \"GET /internal/apm/storage_explorer_summary_stats\" | \"GET /internal/apm/traces/{traceId}/span_links/{spanId}/parents\" | \"GET /internal/apm/traces/{traceId}/span_links/{spanId}/children\" | \"GET /internal/apm/services/{serviceName}/infrastructure_attributes\" | \"GET /internal/apm/debug-telemetry\" | \"GET /internal/apm/time_range_metadata\"" ], "path": "x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts", "deprecated": false, @@ -1064,6 +1064,86 @@ "SpanLinkDetails", "[]; }, ", "APMRouteCreateOptions", + ">; \"GET /internal/apm/storage_explorer_summary_stats\": ", + "ServerRoute", + "<\"GET /internal/apm/storage_explorer_summary_stats\", ", + "TypeC", + "<{ query: ", + "IntersectionC", + "<[", + "TypeC", + "<{ indexLifecyclePhase: ", + "UnionC", + "<[", + "LiteralC", + "<", + "IndexLifecyclePhaseSelectOption", + ".All>, ", + "LiteralC", + "<", + "IndexLifecyclePhaseSelectOption", + ".Hot>, ", + "LiteralC", + "<", + "IndexLifecyclePhaseSelectOption", + ".Warm>, ", + "LiteralC", + "<", + "IndexLifecyclePhaseSelectOption", + ".Cold>, ", + "LiteralC", + "<", + "IndexLifecyclePhaseSelectOption", + ".Frozen>]>; }>, ", + "TypeC", + "<{ probability: ", + "Type", + "; }>, ", + "TypeC", + "<{ environment: ", + "UnionC", + "<[", + "LiteralC", + "<\"ENVIRONMENT_NOT_DEFINED\">, ", + "LiteralC", + "<\"ENVIRONMENT_ALL\">, ", + "BrandC", + "<", + "StringC", + ", ", + "NonEmptyStringBrand", + ">]>; }>, ", + "TypeC", + "<{ kuery: ", + "StringC", + "; }>, ", + "TypeC", + "<{ start: ", + "Type", + "; end: ", + "Type", + "; }>]>; }>, ", + { + "pluginId": "apm", + "scope": "server", + "docId": "kibApmPluginApi", + "section": "def-server.APMRouteHandlerResources", + "text": "APMRouteHandlerResources" + }, + ", { tracesPerMinute: number; numberOfServices: number; estimatedSize: number; dailyDataGeneration: number; }, ", + "APMRouteCreateOptions", + ">; \"GET /internal/apm/storage_explorer/privileges\": ", + "ServerRoute", + "<\"GET /internal/apm/storage_explorer/privileges\", undefined, ", + { + "pluginId": "apm", + "scope": "server", + "docId": "kibApmPluginApi", + "section": "def-server.APMRouteHandlerResources", + "text": "APMRouteHandlerResources" + }, + ", { hasPrivileges: boolean; }, ", + "APMRouteCreateOptions", ">; \"GET /internal/apm/storage_chart\": ", "ServerRoute", "<\"GET /internal/apm/storage_chart\", ", @@ -3564,13 +3644,17 @@ "section": "def-server.APMRouteHandlerResources", "text": "APMRouteHandlerResources" }, - ", { currentPeriod: _.Dictionary<{ transactionName: string; latency: ", + ", { currentPeriod: ", + "Dictionary", + "<{ transactionName: string; latency: ", "Coordinate", "[]; throughput: ", "Coordinate", "[]; errorRate: ", "Coordinate", - "[]; impact: number; }>; previousPeriod: _.Dictionary<{ errorRate: { x: number; y: ", + "[]; impact: number; }>; previousPeriod: ", + "Dictionary", + "<{ errorRate: { x: number; y: ", "Maybe", "; }[]; throughput: { x: number; y: ", "Maybe", @@ -4356,7 +4440,9 @@ "section": "def-server.APMRouteHandlerResources", "text": "APMRouteHandlerResources" }, - ", { currentPeriod: _.Dictionary<{ serviceNodeName: string; errorRate?: ", + ", { currentPeriod: ", + "Dictionary", + "<{ serviceNodeName: string; errorRate?: ", "Coordinate", "[] | undefined; latency?: ", "Coordinate", @@ -4366,7 +4452,9 @@ "Coordinate", "[] | undefined; memoryUsage?: ", "Coordinate", - "[] | undefined; }>; previousPeriod: _.Dictionary<{ cpuUsage: { x: number; y: ", + "[] | undefined; }>; previousPeriod: ", + "Dictionary", + "<{ cpuUsage: { x: number; y: ", "Maybe", "; }[]; errorRate: { x: number; y: ", "Maybe", @@ -4820,7 +4908,11 @@ "section": "def-server.APMRouteHandlerResources", "text": "APMRouteHandlerResources" }, - ", { currentPeriod: _.Dictionary<{ serviceName: string; latency: { x: number; y: number | null; }[]; transactionErrorRate: { x: number; y: number; }[]; throughput: { x: number; y: number; }[]; }>; previousPeriod: _.Dictionary<{ serviceName: string; latency: { x: number; y: number | null; }[]; transactionErrorRate: { x: number; y: number; }[]; throughput: { x: number; y: number; }[]; }>; }, ", + ", { currentPeriod: ", + "Dictionary", + "<{ serviceName: string; latency: { x: number; y: number | null; }[]; transactionErrorRate: { x: number; y: number; }[]; throughput: { x: number; y: number; }[]; }>; previousPeriod: ", + "Dictionary", + "<{ serviceName: string; latency: { x: number; y: number | null; }[]; transactionErrorRate: { x: number; y: number; }[]; throughput: { x: number; y: number; }[]; }>; }, ", "APMRouteCreateOptions", ">; \"GET /internal/apm/services\": ", "ServerRoute", @@ -5468,9 +5560,13 @@ "section": "def-server.APMRouteHandlerResources", "text": "APMRouteHandlerResources" }, - ", { currentPeriod: _.Dictionary<{ groupId: string; timeseries: ", + ", { currentPeriod: ", + "Dictionary", + "<{ groupId: string; timeseries: ", "Coordinate", - "[]; }>; previousPeriod: _.Dictionary<{ timeseries: { x: number; y: ", + "[]; }>; previousPeriod: ", + "Dictionary", + "<{ timeseries: { x: number; y: ", "Maybe", "; }[]; groupId: string; }>; }, ", "APMRouteCreateOptions", diff --git a/api_docs/apm.mdx b/api_docs/apm.mdx index 27c873e1cbc08c..44f6502f2d399b 100644 --- a/api_docs/apm.mdx +++ b/api_docs/apm.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/apm title: "apm" image: https://source.unsplash.com/400x175/?github description: API docs for the apm plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'apm'] --- import apmObj from './apm.devdocs.json'; diff --git a/api_docs/banners.mdx b/api_docs/banners.mdx index 167582665bb6d3..4f9b84e6fad967 100644 --- a/api_docs/banners.mdx +++ b/api_docs/banners.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/banners title: "banners" image: https://source.unsplash.com/400x175/?github description: API docs for the banners plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'banners'] --- import bannersObj from './banners.devdocs.json'; diff --git a/api_docs/bfetch.mdx b/api_docs/bfetch.mdx index 363cb213dfa2c9..3d13ab8ce68c47 100644 --- a/api_docs/bfetch.mdx +++ b/api_docs/bfetch.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/bfetch title: "bfetch" image: https://source.unsplash.com/400x175/?github description: API docs for the bfetch plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'bfetch'] --- import bfetchObj from './bfetch.devdocs.json'; diff --git a/api_docs/canvas.mdx b/api_docs/canvas.mdx index 2910180b02cdf0..fc364ec27df613 100644 --- a/api_docs/canvas.mdx +++ b/api_docs/canvas.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/canvas title: "canvas" image: https://source.unsplash.com/400x175/?github description: API docs for the canvas plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'canvas'] --- import canvasObj from './canvas.devdocs.json'; diff --git a/api_docs/cases.devdocs.json b/api_docs/cases.devdocs.json index e68be19ab72037..b6707b75f20547 100644 --- a/api_docs/cases.devdocs.json +++ b/api_docs/cases.devdocs.json @@ -1544,7 +1544,7 @@ "ConnectorTypes", ".swimlane; fields: { caseId: string | null; } | null; name: string; }; settings: { syncAlerts: boolean; }; owner: string; severity: ", "CaseSeverity", - "; assignees: { uid: string; }[]; duration: number | null; closedAt: string | null; closedBy: { email: string | null | undefined; fullName: string | null | undefined; username: string | null | undefined; } | null; createdAt: string; createdBy: { email: string | null | undefined; fullName: string | null | undefined; username: string | null | undefined; }; externalService: { connectorId: string; connectorName: string; externalId: string; externalTitle: string; externalUrl: string; pushedAt: string; pushedBy: { email: string | null | undefined; fullName: string | null | undefined; username: string | null | undefined; }; } | null; updatedAt: string | null; updatedBy: { email: string | null | undefined; fullName: string | null | undefined; username: string | null | undefined; } | null; id: string; totalComment: number; totalAlerts: number; version: string; comments?: ((({ comment: string; type: ", + "; assignees: { uid: string; }[]; duration: number | null; closedAt: string | null; closedBy: { email: string | null | undefined; fullName: string | null | undefined; username: string | null | undefined; profileUid?: string | undefined; } | null; createdAt: string; createdBy: { email: string | null | undefined; fullName: string | null | undefined; username: string | null | undefined; profileUid?: string | undefined; }; externalService: { connectorId: string; connectorName: string; externalId: string; externalTitle: string; externalUrl: string; pushedAt: string; pushedBy: { email: string | null | undefined; fullName: string | null | undefined; username: string | null | undefined; profileUid?: string | undefined; }; } | null; updatedAt: string | null; updatedBy: { email: string | null | undefined; fullName: string | null | undefined; username: string | null | undefined; profileUid?: string | undefined; } | null; id: string; totalComment: number; totalAlerts: number; version: string; comments?: ((({ comment: string; type: ", { "pluginId": "cases", "scope": "common", @@ -1552,7 +1552,7 @@ "section": "def-common.CommentType", "text": "CommentType" }, - ".user; owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; owner: string; pushed_at: string | null; pushed_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; }) | ({ type: ", + ".user; owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } & { profile_uid?: string | undefined; }; owner: string; pushed_at: string | null; pushed_by: ({ email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } & { profile_uid?: string | undefined; }) | null; updated_at: string | null; updated_by: ({ email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } & { profile_uid?: string | undefined; }) | null; }) | ({ type: ", { "pluginId": "cases", "scope": "common", @@ -1560,7 +1560,7 @@ "section": "def-common.CommentType", "text": "CommentType" }, - ".alert; alertId: string | string[]; index: string | string[]; rule: { id: string | null; name: string | null; }; owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; owner: string; pushed_at: string | null; pushed_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; }) | ({ type: ", + ".alert; alertId: string | string[]; index: string | string[]; rule: { id: string | null; name: string | null; }; owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } & { profile_uid?: string | undefined; }; owner: string; pushed_at: string | null; pushed_by: ({ email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } & { profile_uid?: string | undefined; }) | null; updated_at: string | null; updated_by: ({ email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } & { profile_uid?: string | undefined; }) | null; }) | ({ type: ", { "pluginId": "cases", "scope": "common", @@ -1568,7 +1568,7 @@ "section": "def-common.CommentType", "text": "CommentType" }, - ".actions; comment: string; actions: { targets: { hostname: string; endpointId: string; }[]; type: string; }; owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; owner: string; pushed_at: string | null; pushed_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; }) | (({ externalReferenceId: string; externalReferenceStorage: { type: ", + ".actions; comment: string; actions: { targets: { hostname: string; endpointId: string; }[]; type: string; }; owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } & { profile_uid?: string | undefined; }; owner: string; pushed_at: string | null; pushed_by: ({ email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } & { profile_uid?: string | undefined; }) | null; updated_at: string | null; updated_by: ({ email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } & { profile_uid?: string | undefined; }) | null; }) | (({ externalReferenceId: string; externalReferenceStorage: { type: ", "ExternalReferenceStorageType", ".elasticSearchDoc; }; externalReferenceAttachmentTypeId: string; externalReferenceMetadata: { [x: string]: ", "JsonValue", @@ -1592,7 +1592,7 @@ "section": "def-common.CommentType", "text": "CommentType" }, - ".externalReference; owner: string; }) & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; owner: string; pushed_at: string | null; pushed_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; }) | ({ type: ", + ".externalReference; owner: string; }) & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } & { profile_uid?: string | undefined; }; owner: string; pushed_at: string | null; pushed_by: ({ email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } & { profile_uid?: string | undefined; }) | null; updated_at: string | null; updated_by: ({ email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } & { profile_uid?: string | undefined; }) | null; }) | ({ type: ", { "pluginId": "cases", "scope": "common", @@ -1602,7 +1602,7 @@ }, ".persistableState; owner: string; persistableStateAttachmentTypeId: string; persistableStateAttachmentState: { [x: string]: ", "JsonValue", - "; }; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; owner: string; pushed_at: string | null; pushed_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; })) & { id: string; version: string; })[] | undefined; }, \"comments\"> & { comments: ", + "; }; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } & { profile_uid?: string | undefined; }; owner: string; pushed_at: string | null; pushed_by: ({ email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } & { profile_uid?: string | undefined; }) | null; updated_at: string | null; updated_by: ({ email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } & { profile_uid?: string | undefined; }) | null; })) & { id: string; version: string; })[] | undefined; }, \"comments\"> & { comments: ", "Comment", "[]; }" ], diff --git a/api_docs/cases.mdx b/api_docs/cases.mdx index cbc8bc0dcfd440..23cb33085cf2a3 100644 --- a/api_docs/cases.mdx +++ b/api_docs/cases.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cases title: "cases" image: https://source.unsplash.com/400x175/?github description: API docs for the cases plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cases'] --- import casesObj from './cases.devdocs.json'; diff --git a/api_docs/charts.mdx b/api_docs/charts.mdx index ca9fc19e1213bb..bf7d2e27f5e864 100644 --- a/api_docs/charts.mdx +++ b/api_docs/charts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/charts title: "charts" image: https://source.unsplash.com/400x175/?github description: API docs for the charts plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'charts'] --- import chartsObj from './charts.devdocs.json'; diff --git a/api_docs/cloud.mdx b/api_docs/cloud.mdx index bffc54e20d6abc..308305653f9138 100644 --- a/api_docs/cloud.mdx +++ b/api_docs/cloud.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloud title: "cloud" image: https://source.unsplash.com/400x175/?github description: API docs for the cloud plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloud'] --- import cloudObj from './cloud.devdocs.json'; diff --git a/api_docs/cloud_security_posture.mdx b/api_docs/cloud_security_posture.mdx index 40ee284d2734c8..3b6e57d3ba40a9 100644 --- a/api_docs/cloud_security_posture.mdx +++ b/api_docs/cloud_security_posture.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudSecurityPosture title: "cloudSecurityPosture" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudSecurityPosture plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudSecurityPosture'] --- import cloudSecurityPostureObj from './cloud_security_posture.devdocs.json'; diff --git a/api_docs/console.mdx b/api_docs/console.mdx index 88390589d38352..eae54f4bf225f4 100644 --- a/api_docs/console.mdx +++ b/api_docs/console.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/console title: "console" image: https://source.unsplash.com/400x175/?github description: API docs for the console plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'console'] --- import consoleObj from './console.devdocs.json'; diff --git a/api_docs/controls.devdocs.json b/api_docs/controls.devdocs.json index 78f856d45261e6..0a54b225251521 100644 --- a/api_docs/controls.devdocs.json +++ b/api_docs/controls.devdocs.json @@ -448,9 +448,7 @@ "section": "def-public.ControlOutput", "text": "ControlOutput" }, - ">, ", - "SavedObjectAttributes", - ">, partial?: Partial) => ", + ">, unknown>, partial?: Partial) => ", { "pluginId": "controls", "scope": "common", @@ -507,9 +505,7 @@ "section": "def-public.ControlOutput", "text": "ControlOutput" }, - ">, ", - "SavedObjectAttributes", - ">" + ">, unknown>" ], "path": "src/plugins/controls/public/control_group/embeddable/control_group_container.tsx", "deprecated": false, @@ -736,9 +732,7 @@ "section": "def-public.EmbeddableOutput", "text": "EmbeddableOutput" }, - ">, ", - "SavedObjectAttributes", - ">" + ">, unknown>" ], "path": "src/plugins/controls/public/control_group/embeddable/control_group_container_factory.ts", "deprecated": false, @@ -1431,9 +1425,7 @@ "section": "def-public.EmbeddableOutput", "text": "EmbeddableOutput" }, - ">, ", - "SavedObjectAttributes", - ">,", + ">, unknown>,", { "pluginId": "controls", "scope": "public", @@ -2341,9 +2333,7 @@ "section": "def-public.EmbeddableOutput", "text": "EmbeddableOutput" }, - ">, ", - "SavedObjectAttributes", - ">,", + ">, unknown>,", { "pluginId": "controls", "scope": "public", @@ -3608,9 +3598,7 @@ "section": "def-public.ControlOutput", "text": "ControlOutput" }, - ">, ", - "SavedObjectAttributes", - ">" + ">, unknown>" ], "path": "src/plugins/controls/public/types.ts", "deprecated": false, diff --git a/api_docs/controls.mdx b/api_docs/controls.mdx index ae609d41255ab2..1af11290292640 100644 --- a/api_docs/controls.mdx +++ b/api_docs/controls.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/controls title: "controls" image: https://source.unsplash.com/400x175/?github description: API docs for the controls plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'controls'] --- import controlsObj from './controls.devdocs.json'; diff --git a/api_docs/core.devdocs.json b/api_docs/core.devdocs.json index 35d2b408052f50..7000bba27c6fb1 100644 --- a/api_docs/core.devdocs.json +++ b/api_docs/core.devdocs.json @@ -576,6 +576,10 @@ "plugin": "@kbn/core-analytics-server-internal", "path": "packages/core/analytics/core-analytics-server-internal/src/analytics_service.ts" }, + { + "plugin": "@kbn/core-status-server-internal", + "path": "packages/core/status/core-status-server-internal/src/status_service.ts" + }, { "plugin": "security", "path": "x-pack/plugins/security/server/analytics/analytics_service.test.ts" @@ -588,6 +592,18 @@ "plugin": "@kbn/core-analytics-server-mocks", "path": "packages/core/analytics/core-analytics-server-mocks/src/analytics_service.mock.ts" }, + { + "plugin": "@kbn/core-status-server-internal", + "path": "packages/core/status/core-status-server-internal/src/status_service.test.ts" + }, + { + "plugin": "@kbn/core-status-server-internal", + "path": "packages/core/status/core-status-server-internal/src/status_service.test.ts" + }, + { + "plugin": "@kbn/core-status-server-internal", + "path": "packages/core/status/core-status-server-internal/src/status_service.test.ts" + }, { "plugin": "@kbn/core-analytics-browser-mocks", "path": "packages/core/analytics/core-analytics-browser-mocks/src/analytics_service.mock.ts" @@ -918,6 +934,10 @@ "plugin": "@kbn/core-execution-context-browser-internal", "path": "packages/core/execution-context/core-execution-context-browser-internal/src/execution_context_service.ts" }, + { + "plugin": "@kbn/core-status-server-internal", + "path": "packages/core/status/core-status-server-internal/src/status_service.ts" + }, { "plugin": "cloud", "path": "x-pack/plugins/cloud/public/plugin.test.ts" @@ -982,6 +1002,14 @@ "plugin": "@kbn/core-elasticsearch-server-internal", "path": "packages/core/elasticsearch/core-elasticsearch-server-internal/src/register_analytics_context_provider.test.ts" }, + { + "plugin": "@kbn/core-status-server-internal", + "path": "packages/core/status/core-status-server-internal/src/status_service.test.ts" + }, + { + "plugin": "@kbn/core-status-server-internal", + "path": "packages/core/status/core-status-server-internal/src/status_service.test.ts" + }, { "plugin": "@kbn/core-analytics-browser-internal", "path": "packages/core/analytics/core-analytics-browser-internal/src/analytics_service.test.mocks.ts" @@ -10559,7 +10587,7 @@ "tags": [], "label": "attributes", "description": [ - "{@inheritdoc SavedObjectAttributes}" + "The data for a Saved Object is stored as an object in the `attributes` property." ], "signature": [ "T" @@ -10657,7 +10685,9 @@ "parentPluginId": "core", "id": "def-public.SavedObjectAttributes", "type": "Interface", - "tags": [], + "tags": [ + "deprecated" + ], "label": "SavedObjectAttributes", "description": [ "\nThe data for a Saved Object is stored as an object in the `attributes`\nproperty.\n" @@ -10666,8 +10696,590 @@ "SavedObjectAttributes" ], "path": "node_modules/@types/kbn__core-saved-objects-common/index.d.ts", - "deprecated": false, + "deprecated": true, "trackAdoption": false, + "references": [ + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/common/types.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/common/types.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts" + }, + { + "plugin": "ml", + "path": "x-pack/plugins/ml/common/types/modules.ts" + }, + { + "plugin": "ml", + "path": "x-pack/plugins/ml/common/types/modules.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/actions_client.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/actions_client.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/actions_client.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/actions_client.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/actions_client.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/actions_client.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/actions_client.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/types.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/types.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/types.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/types.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/types.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/types.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/common/rule.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/common/rule.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/common/rule.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/common/rule.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/common/rule.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/common/rule.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/saved_objects/geo_containment/migrations.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/saved_objects/geo_containment/migrations.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/saved_objects/migrations.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/saved_objects/migrations.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/saved_objects/migrations.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/types.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/types.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/types.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/types.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/types.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/types.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/types.ts" + }, + { + "plugin": "canvas", + "path": "x-pack/plugins/canvas/server/routes/custom_elements/find.ts" + }, + { + "plugin": "canvas", + "path": "x-pack/plugins/canvas/server/routes/custom_elements/find.ts" + }, + { + "plugin": "canvas", + "path": "x-pack/plugins/canvas/server/routes/workpad/find.ts" + }, + { + "plugin": "canvas", + "path": "x-pack/plugins/canvas/server/routes/workpad/find.ts" + }, + { + "plugin": "enterpriseSearch", + "path": "x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts" + }, + { + "plugin": "enterpriseSearch", + "path": "x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts" + }, + { + "plugin": "enterpriseSearch", + "path": "x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts" + }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_types.ts" + }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_types.ts" + }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.ts" + }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.ts" + }, + { + "plugin": "taskManager", + "path": "x-pack/plugins/task_manager/server/task_store.test.ts" + }, + { + "plugin": "taskManager", + "path": "x-pack/plugins/task_manager/server/task_store.test.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/server/usage/dashboard_telemetry_collection_task.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/server/usage/dashboard_telemetry_collection_task.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/server/usage/dashboard_telemetry.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/server/usage/dashboard_telemetry.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/server/usage/find_by_value_embeddables.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/server/usage/find_by_value_embeddables.ts" + }, + { + "plugin": "savedSearch", + "path": "src/plugins/saved_search/server/saved_objects/search_migrations.ts" + }, + { + "plugin": "savedSearch", + "path": "src/plugins/saved_search/server/saved_objects/search_migrations.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/types.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/types.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/types.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/helpers/create_source.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/helpers/create_source.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/helpers/find_object_by_title.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/helpers/find_object_by_title.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.ts" + }, + { + "plugin": "embeddable", + "path": "src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx" + }, + { + "plugin": "embeddable", + "path": "src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx" + }, + { + "plugin": "embeddable", + "path": "src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx" + }, + { + "plugin": "embeddable", + "path": "src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts" + }, + { + "plugin": "embeddable", + "path": "src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts" + }, + { + "plugin": "embeddable", + "path": "src/plugins/embeddable/public/types.ts" + }, + { + "plugin": "embeddable", + "path": "src/plugins/embeddable/public/types.ts" + }, + { + "plugin": "fleet", + "path": "x-pack/plugins/fleet/common/types/models/epm.ts" + }, + { + "plugin": "fleet", + "path": "x-pack/plugins/fleet/common/types/models/epm.ts" + }, + { + "plugin": "fleet", + "path": "x-pack/plugins/fleet/common/types/models/settings.ts" + }, + { + "plugin": "fleet", + "path": "x-pack/plugins/fleet/common/types/models/settings.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_objects_utils/find_object_by_title.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_objects_utils/find_object_by_title.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_visualization_references/saved_visualization_references.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_visualization_references/saved_visualization_references.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_visualize_utils.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_visualize_utils.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_visualize_utils.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_visualize_utils.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_visualize_utils.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_visualize_utils.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/wizard/search_selection/search_selection.tsx" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/wizard/search_selection/search_selection.tsx" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/hooks/use_get_saved_object.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/hooks/use_get_saved_object.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx" + }, + { + "plugin": "canvas", + "path": "x-pack/plugins/canvas/shareable_runtime/types.ts" + }, + { + "plugin": "canvas", + "path": "x-pack/plugins/canvas/shareable_runtime/types.ts" + }, + { + "plugin": "graph", + "path": "x-pack/plugins/graph/public/services/persistence/saved_workspace_references.ts" + }, + { + "plugin": "graph", + "path": "x-pack/plugins/graph/public/services/persistence/saved_workspace_references.ts" + }, + { + "plugin": "graph", + "path": "x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts" + }, + { + "plugin": "graph", + "path": "x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.test.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.test.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.test.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.test.ts" + }, + { + "plugin": "@kbn/core-saved-objects-server-internal", + "path": "packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/collect_references_deep.test.ts" + }, + { + "plugin": "@kbn/core-saved-objects-server-internal", + "path": "packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/collect_references_deep.test.ts" + } + ], "children": [ { "parentPluginId": "core", @@ -14920,16 +15532,13 @@ { "parentPluginId": "core", "id": "def-public.URL_MAX_LENGTH", - "type": "CompoundType", + "type": "number", "tags": [], "label": "URL_MAX_LENGTH", "description": [ "\nThe max URL length allowed by the current browser. Should be used to display warnings to users when query parameters\ncause URL to exceed this limit." ], - "signature": [ - "2000 | 25000" - ], - "path": "src/core/public/core_app/errors/url_overflow.tsx", + "path": "node_modules/@types/kbn__core-apps-browser-internal/index.d.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -18603,6 +19212,10 @@ "plugin": "@kbn/core-analytics-server-internal", "path": "packages/core/analytics/core-analytics-server-internal/src/analytics_service.ts" }, + { + "plugin": "@kbn/core-status-server-internal", + "path": "packages/core/status/core-status-server-internal/src/status_service.ts" + }, { "plugin": "security", "path": "x-pack/plugins/security/server/analytics/analytics_service.test.ts" @@ -18615,6 +19228,18 @@ "plugin": "@kbn/core-analytics-server-mocks", "path": "packages/core/analytics/core-analytics-server-mocks/src/analytics_service.mock.ts" }, + { + "plugin": "@kbn/core-status-server-internal", + "path": "packages/core/status/core-status-server-internal/src/status_service.test.ts" + }, + { + "plugin": "@kbn/core-status-server-internal", + "path": "packages/core/status/core-status-server-internal/src/status_service.test.ts" + }, + { + "plugin": "@kbn/core-status-server-internal", + "path": "packages/core/status/core-status-server-internal/src/status_service.test.ts" + }, { "plugin": "@kbn/core-analytics-browser-mocks", "path": "packages/core/analytics/core-analytics-browser-mocks/src/analytics_service.mock.ts" @@ -18945,6 +19570,10 @@ "plugin": "@kbn/core-execution-context-browser-internal", "path": "packages/core/execution-context/core-execution-context-browser-internal/src/execution_context_service.ts" }, + { + "plugin": "@kbn/core-status-server-internal", + "path": "packages/core/status/core-status-server-internal/src/status_service.ts" + }, { "plugin": "cloud", "path": "x-pack/plugins/cloud/public/plugin.test.ts" @@ -19009,6 +19638,14 @@ "plugin": "@kbn/core-elasticsearch-server-internal", "path": "packages/core/elasticsearch/core-elasticsearch-server-internal/src/register_analytics_context_provider.test.ts" }, + { + "plugin": "@kbn/core-status-server-internal", + "path": "packages/core/status/core-status-server-internal/src/status_service.test.ts" + }, + { + "plugin": "@kbn/core-status-server-internal", + "path": "packages/core/status/core-status-server-internal/src/status_service.test.ts" + }, { "plugin": "@kbn/core-analytics-browser-internal", "path": "packages/core/analytics/core-analytics-browser-internal/src/analytics_service.test.mocks.ts" @@ -21123,13 +21760,7 @@ "{@link StatusServiceSetup}" ], "signature": [ - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.StatusServiceSetup", - "text": "StatusServiceSetup" - } + "StatusServiceSetup" ], "path": "src/core/server/index.ts", "deprecated": false, @@ -21383,7 +22014,10 @@ "description": [ "\nStatus of core services.\n" ], - "path": "src/core/server/status/types.ts", + "signature": [ + "CoreStatus" + ], + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", "deprecated": false, "trackAdoption": false, "children": [ @@ -21398,7 +22032,7 @@ "ServiceStatus", "" ], - "path": "src/core/server/status/types.ts", + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -21413,7 +22047,7 @@ "ServiceStatus", "" ], - "path": "src/core/server/status/types.ts", + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", "deprecated": false, "trackAdoption": false } @@ -36019,6 +36653,30 @@ "plugin": "kibanaUsageCollection", "path": "src/plugins/kibana_usage_collection/server/collectors/ops_stats/ops_stats_collector.ts" }, + { + "plugin": "@kbn/core-apps-browser-internal", + "path": "packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts" + }, + { + "plugin": "@kbn/core-apps-browser-internal", + "path": "packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts" + }, + { + "plugin": "@kbn/core-apps-browser-internal", + "path": "packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts" + }, + { + "plugin": "@kbn/core-apps-browser-internal", + "path": "packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts" + }, + { + "plugin": "@kbn/core-apps-browser-internal", + "path": "packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts" + }, + { + "plugin": "@kbn/core-apps-browser-internal", + "path": "packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts" + }, { "plugin": "@kbn/core-metrics-server-internal", "path": "packages/core/metrics/core-metrics-server-internal/src/ops_metrics_collector.ts" @@ -36027,6 +36685,14 @@ "plugin": "@kbn/core-metrics-server-internal", "path": "packages/core/metrics/core-metrics-server-internal/src/logging/get_ops_metrics_log.ts" }, + { + "plugin": "@kbn/core-status-server-internal", + "path": "packages/core/status/core-status-server-internal/src/routes/status.ts" + }, + { + "plugin": "@kbn/core-status-server-internal", + "path": "packages/core/status/core-status-server-internal/src/routes/status.ts" + }, { "plugin": "@kbn/core-usage-data-server-internal", "path": "packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.ts" @@ -36042,6 +36708,10 @@ { "plugin": "@kbn/core-metrics-server-internal", "path": "packages/core/metrics/core-metrics-server-internal/src/logging/get_ops_metrics_log.test.ts" + }, + { + "plugin": "@kbn/core-apps-browser-internal", + "path": "packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts" } ] }, @@ -36932,13 +37602,13 @@ "signature": [ "{ legacy: { globalConfig$: ", "Observable", - " moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; }>; readonly requestTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; }>; readonly pingTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; }>; }>; path: Readonly<{ readonly data: string; }>; savedObjects: Readonly<{ readonly maxImportPayloadBytes: Readonly<{ isGreaterThan: (other: ", + " moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; }>; readonly shardTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; }>; readonly pingTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; }>; }>; path: Readonly<{ readonly data: string; }>; savedObjects: Readonly<{ readonly maxImportPayloadBytes: Readonly<{ isGreaterThan: (other: ", "ByteSizeValue", ") => boolean; isLessThan: (other: ", "ByteSizeValue", ") => boolean; isEqualTo: (other: ", "ByteSizeValue", - ") => boolean; getValueInBytes: () => number; toString: (returnUnit?: ByteSizeValueUnit | undefined) => string; }>; }>; }>>; get: () => Readonly<{ elasticsearch: Readonly<{ readonly shardTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; }>; readonly requestTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; }>; readonly pingTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; }>; }>; path: Readonly<{ readonly data: string; }>; savedObjects: Readonly<{ readonly maxImportPayloadBytes: Readonly<{ isGreaterThan: (other: ", + ") => boolean; getValueInBytes: () => number; toString: (returnUnit?: ByteSizeValueUnit | undefined) => string; }>; }>; }>>; get: () => Readonly<{ elasticsearch: Readonly<{ readonly requestTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; }>; readonly shardTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; }>; readonly pingTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; }>; }>; path: Readonly<{ readonly data: string; }>; savedObjects: Readonly<{ readonly maxImportPayloadBytes: Readonly<{ isGreaterThan: (other: ", "ByteSizeValue", ") => boolean; isLessThan: (other: ", "ByteSizeValue", @@ -39258,7 +39928,7 @@ "tags": [], "label": "attributes", "description": [ - "{@inheritdoc SavedObjectAttributes}" + "The data for a Saved Object is stored as an object in the `attributes` property." ], "signature": [ "T" @@ -39356,7 +40026,9 @@ "parentPluginId": "core", "id": "def-server.SavedObjectAttributes", "type": "Interface", - "tags": [], + "tags": [ + "deprecated" + ], "label": "SavedObjectAttributes", "description": [ "\nThe data for a Saved Object is stored as an object in the `attributes`\nproperty.\n" @@ -39365,8 +40037,590 @@ "SavedObjectAttributes" ], "path": "node_modules/@types/kbn__core-saved-objects-common/index.d.ts", - "deprecated": false, + "deprecated": true, "trackAdoption": false, + "references": [ + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/common/types.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/common/types.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts" + }, + { + "plugin": "ml", + "path": "x-pack/plugins/ml/common/types/modules.ts" + }, + { + "plugin": "ml", + "path": "x-pack/plugins/ml/common/types/modules.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/actions_client.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/actions_client.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/actions_client.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/actions_client.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/actions_client.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/actions_client.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/actions_client.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/types.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/types.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/types.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/types.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/types.ts" + }, + { + "plugin": "actions", + "path": "x-pack/plugins/actions/server/types.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/common/rule.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/common/rule.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/common/rule.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/common/rule.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/common/rule.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/common/rule.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/saved_objects/geo_containment/migrations.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/saved_objects/geo_containment/migrations.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/saved_objects/migrations.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/saved_objects/migrations.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/saved_objects/migrations.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/types.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/types.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/types.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/types.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/types.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/types.ts" + }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/types.ts" + }, + { + "plugin": "canvas", + "path": "x-pack/plugins/canvas/server/routes/custom_elements/find.ts" + }, + { + "plugin": "canvas", + "path": "x-pack/plugins/canvas/server/routes/custom_elements/find.ts" + }, + { + "plugin": "canvas", + "path": "x-pack/plugins/canvas/server/routes/workpad/find.ts" + }, + { + "plugin": "canvas", + "path": "x-pack/plugins/canvas/server/routes/workpad/find.ts" + }, + { + "plugin": "enterpriseSearch", + "path": "x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts" + }, + { + "plugin": "enterpriseSearch", + "path": "x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts" + }, + { + "plugin": "enterpriseSearch", + "path": "x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts" + }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_types.ts" + }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_types.ts" + }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.ts" + }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.ts" + }, + { + "plugin": "taskManager", + "path": "x-pack/plugins/task_manager/server/task_store.test.ts" + }, + { + "plugin": "taskManager", + "path": "x-pack/plugins/task_manager/server/task_store.test.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/server/usage/dashboard_telemetry_collection_task.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/server/usage/dashboard_telemetry_collection_task.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/server/usage/dashboard_telemetry.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/server/usage/dashboard_telemetry.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/server/usage/find_by_value_embeddables.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/server/usage/find_by_value_embeddables.ts" + }, + { + "plugin": "savedSearch", + "path": "src/plugins/saved_search/server/saved_objects/search_migrations.ts" + }, + { + "plugin": "savedSearch", + "path": "src/plugins/saved_search/server/saved_objects/search_migrations.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/types.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/types.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/types.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/helpers/create_source.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/helpers/create_source.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/helpers/find_object_by_title.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/helpers/find_object_by_title.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.ts" + }, + { + "plugin": "embeddable", + "path": "src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx" + }, + { + "plugin": "embeddable", + "path": "src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx" + }, + { + "plugin": "embeddable", + "path": "src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx" + }, + { + "plugin": "embeddable", + "path": "src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts" + }, + { + "plugin": "embeddable", + "path": "src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts" + }, + { + "plugin": "embeddable", + "path": "src/plugins/embeddable/public/types.ts" + }, + { + "plugin": "embeddable", + "path": "src/plugins/embeddable/public/types.ts" + }, + { + "plugin": "fleet", + "path": "x-pack/plugins/fleet/common/types/models/epm.ts" + }, + { + "plugin": "fleet", + "path": "x-pack/plugins/fleet/common/types/models/epm.ts" + }, + { + "plugin": "fleet", + "path": "x-pack/plugins/fleet/common/types/models/settings.ts" + }, + { + "plugin": "fleet", + "path": "x-pack/plugins/fleet/common/types/models/settings.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_objects_utils/find_object_by_title.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_objects_utils/find_object_by_title.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_visualization_references/saved_visualization_references.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_visualization_references/saved_visualization_references.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_visualize_utils.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_visualize_utils.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_visualize_utils.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_visualize_utils.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_visualize_utils.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_visualize_utils.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/wizard/search_selection/search_selection.tsx" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/wizard/search_selection/search_selection.tsx" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/hooks/use_get_saved_object.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/hooks/use_get_saved_object.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx" + }, + { + "plugin": "canvas", + "path": "x-pack/plugins/canvas/shareable_runtime/types.ts" + }, + { + "plugin": "canvas", + "path": "x-pack/plugins/canvas/shareable_runtime/types.ts" + }, + { + "plugin": "graph", + "path": "x-pack/plugins/graph/public/services/persistence/saved_workspace_references.ts" + }, + { + "plugin": "graph", + "path": "x-pack/plugins/graph/public/services/persistence/saved_workspace_references.ts" + }, + { + "plugin": "graph", + "path": "x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts" + }, + { + "plugin": "graph", + "path": "x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/saved_object.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.test.ts" + }, + { + "plugin": "savedObjects", + "path": "src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.test.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.test.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.test.ts" + }, + { + "plugin": "visualizations", + "path": "src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.test.ts" + }, + { + "plugin": "@kbn/core-saved-objects-server-internal", + "path": "packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/collect_references_deep.test.ts" + }, + { + "plugin": "@kbn/core-saved-objects-server-internal", + "path": "packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/collect_references_deep.test.ts" + } + ], "children": [ { "parentPluginId": "core", @@ -40213,7 +41467,7 @@ "tags": [], "label": "attributes", "description": [ - "{@inheritdoc SavedObjectAttributes}" + "The data for a Saved Object is stored as an object in the `attributes` property." ], "signature": [ "{ [P in keyof T]?: T[P] | undefined; }" @@ -45149,9 +46403,7 @@ "\nRegister a {@link SavedObjectsType | savedObjects type} definition.\n\nSee the {@link SavedObjectsTypeMappingDefinition | mappings format} and\n{@link SavedObjectMigrationMap | migration format} for more details about these.\n" ], "signature": [ - "(type: ", + "(type: ", "SavedObjectsType", ") => void" ], @@ -46420,7 +47672,7 @@ "ServiceStatus", "" ], - "path": "node_modules/@types/kbn__core-base-common/index.d.ts", + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", "deprecated": false, "trackAdoption": false, "children": [ @@ -46436,7 +47688,7 @@ "signature": [ "Readonly<{ toString: () => \"available\"; valueOf: () => 0; toJSON: () => \"available\"; }> | Readonly<{ toString: () => \"degraded\"; valueOf: () => 1; toJSON: () => \"degraded\"; }> | Readonly<{ toString: () => \"unavailable\"; valueOf: () => 2; toJSON: () => \"unavailable\"; }> | Readonly<{ toString: () => \"critical\"; valueOf: () => 3; toJSON: () => \"critical\"; }>" ], - "path": "node_modules/@types/kbn__core-base-common/index.d.ts", + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -46449,7 +47701,7 @@ "description": [ "\nA high-level summary of the service status." ], - "path": "node_modules/@types/kbn__core-base-common/index.d.ts", + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -46465,7 +47717,7 @@ "signature": [ "string | undefined" ], - "path": "node_modules/@types/kbn__core-base-common/index.d.ts", + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -46481,7 +47733,7 @@ "signature": [ "string | undefined" ], - "path": "node_modules/@types/kbn__core-base-common/index.d.ts", + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -46497,7 +47749,7 @@ "signature": [ "Meta | undefined" ], - "path": "node_modules/@types/kbn__core-base-common/index.d.ts", + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", "deprecated": false, "trackAdoption": false } @@ -46906,7 +48158,10 @@ "description": [ "\nAPI for accessing status of Core and this plugin's dependencies as well as for customizing this plugin's status.\n" ], - "path": "src/core/server/status/types.ts", + "signature": [ + "StatusServiceSetup" + ], + "path": "node_modules/@types/kbn__core-status-server/index.d.ts", "deprecated": false, "trackAdoption": false, "children": [ @@ -46922,16 +48177,10 @@ "signature": [ "Observable", "<", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.CoreStatus", - "text": "CoreStatus" - }, + "CoreStatus", ">" ], - "path": "src/core/server/status/types.ts", + "path": "node_modules/@types/kbn__core-status-server/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -46950,7 +48199,7 @@ "ServiceStatus", ">" ], - "path": "src/core/server/status/types.ts", + "path": "node_modules/@types/kbn__core-status-server/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -46970,7 +48219,7 @@ "ServiceStatus", ">) => void" ], - "path": "src/core/server/status/types.ts", + "path": "node_modules/@types/kbn__core-status-server/index.d.ts", "deprecated": false, "trackAdoption": false, "children": [ @@ -46987,7 +48236,7 @@ "ServiceStatus", ">" ], - "path": "src/core/server/status/types.ts", + "path": "node_modules/@types/kbn__core-status-server/index.d.ts", "deprecated": false, "trackAdoption": false, "isRequired": true @@ -47010,7 +48259,7 @@ "ServiceStatus", ">>" ], - "path": "src/core/server/status/types.ts", + "path": "node_modules/@types/kbn__core-status-server/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -47029,7 +48278,7 @@ "ServiceStatus", ">" ], - "path": "src/core/server/status/types.ts", + "path": "node_modules/@types/kbn__core-status-server/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -47045,7 +48294,7 @@ "signature": [ "() => boolean" ], - "path": "src/core/server/status/types.ts", + "path": "node_modules/@types/kbn__core-status-server/index.d.ts", "deprecated": false, "trackAdoption": false, "children": [], @@ -48623,7 +49872,7 @@ "label": "EcsEventKind", "description": [], "signature": [ - "\"metric\" | \"alert\" | \"state\" | \"event\" | \"signal\" | \"pipeline_error\"" + "\"metric\" | \"alert\" | \"signal\" | \"state\" | \"event\" | \"pipeline_error\"" ], "path": "node_modules/@types/kbn__logging/index.d.ts", "deprecated": false, @@ -52214,7 +53463,7 @@ "signature": [ "Readonly<{ toString: () => \"available\"; valueOf: () => 0; toJSON: () => \"available\"; }> | Readonly<{ toString: () => \"degraded\"; valueOf: () => 1; toJSON: () => \"degraded\"; }> | Readonly<{ toString: () => \"unavailable\"; valueOf: () => 2; toJSON: () => \"unavailable\"; }> | Readonly<{ toString: () => \"critical\"; valueOf: () => 3; toJSON: () => \"critical\"; }>" ], - "path": "node_modules/@types/kbn__core-base-common/index.d.ts", + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -52227,7 +53476,7 @@ "label": "SharedGlobalConfig", "description": [], "signature": [ - "{ readonly elasticsearch: Readonly<{ readonly shardTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; }>; readonly requestTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; }>; readonly pingTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; }>; }>; readonly path: Readonly<{ readonly data: string; }>; readonly savedObjects: Readonly<{ readonly maxImportPayloadBytes: Readonly<{ isGreaterThan: (other: ", + "{ readonly elasticsearch: Readonly<{ readonly requestTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; }>; readonly shardTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; }>; readonly pingTimeout: Readonly<{ clone: () => moment.Duration; humanize: { (argWithSuffix?: boolean | undefined, argThresholds?: moment.argThresholdOpts | undefined): string; (argThresholds?: moment.argThresholdOpts | undefined): string; }; abs: () => moment.Duration; as: (units: moment.unitOfTime.Base) => number; get: (units: moment.unitOfTime.Base) => number; milliseconds: () => number; asMilliseconds: () => number; seconds: () => number; asSeconds: () => number; minutes: () => number; asMinutes: () => number; hours: () => number; asHours: () => number; days: () => number; asDays: () => number; weeks: () => number; asWeeks: () => number; months: () => number; asMonths: () => number; years: () => number; asYears: () => number; add: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; subtract: (inp?: moment.DurationInputArg1, unit?: moment.unitOfTime.DurationConstructor | undefined) => moment.Duration; locale: { (): string; (locale: moment.LocaleSpecifier): moment.Duration; }; localeData: () => moment.Locale; toISOString: () => string; toJSON: () => string; isValid: () => boolean; lang: { (locale: moment.LocaleSpecifier): moment.Moment; (): moment.Locale; }; toIsoString: () => string; }>; }>; readonly path: Readonly<{ readonly data: string; }>; readonly savedObjects: Readonly<{ readonly maxImportPayloadBytes: Readonly<{ isGreaterThan: (other: ", "ByteSizeValue", ") => boolean; isLessThan: (other: ", "ByteSizeValue", @@ -52405,7 +53654,7 @@ "signature": [ "{ readonly available: Readonly<{ toString: () => \"available\"; valueOf: () => 0; toJSON: () => \"available\"; }>; readonly degraded: Readonly<{ toString: () => \"degraded\"; valueOf: () => 1; toJSON: () => \"degraded\"; }>; readonly unavailable: Readonly<{ toString: () => \"unavailable\"; valueOf: () => 2; toJSON: () => \"unavailable\"; }>; readonly critical: Readonly<{ toString: () => \"critical\"; valueOf: () => 3; toJSON: () => \"critical\"; }>; }" ], - "path": "node_modules/@types/kbn__core-base-common/index.d.ts", + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false diff --git a/api_docs/core.mdx b/api_docs/core.mdx index 1bd545035359a8..25b08ab39bc4ef 100644 --- a/api_docs/core.mdx +++ b/api_docs/core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/core title: "core" image: https://source.unsplash.com/400x175/?github description: API docs for the core plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'core'] --- import coreObj from './core.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) for que | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 2657 | 1 | 61 | 2 | +| 2657 | 1 | 58 | 2 | ## Client diff --git a/api_docs/custom_integrations.mdx b/api_docs/custom_integrations.mdx index daf36243e408dd..d7499b10179744 100644 --- a/api_docs/custom_integrations.mdx +++ b/api_docs/custom_integrations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/customIntegrations title: "customIntegrations" image: https://source.unsplash.com/400x175/?github description: API docs for the customIntegrations plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'customIntegrations'] --- import customIntegrationsObj from './custom_integrations.devdocs.json'; diff --git a/api_docs/dashboard.devdocs.json b/api_docs/dashboard.devdocs.json index 7af8bf23811e45..50bbfed86e0a82 100644 --- a/api_docs/dashboard.devdocs.json +++ b/api_docs/dashboard.devdocs.json @@ -348,9 +348,7 @@ "section": "def-public.EmbeddableFactory", "text": "EmbeddableFactory" }, - ", partial?: Partial) => ", + ", partial?: Partial) => ", "DashboardPanelState", "" ], @@ -373,9 +371,7 @@ "section": "def-public.EmbeddableFactory", "text": "EmbeddableFactory" }, - "" + "" ], "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", "deprecated": false, @@ -829,9 +825,7 @@ "section": "def-public.DashboardContainer", "text": "DashboardContainer" }, - ", ", - "SavedObjectAttributes", - ">" + ", unknown>" ], "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx", "deprecated": false, diff --git a/api_docs/dashboard.mdx b/api_docs/dashboard.mdx index d7da06c8bda6e6..bbeb81fe63e2ab 100644 --- a/api_docs/dashboard.mdx +++ b/api_docs/dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dashboard title: "dashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the dashboard plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboard'] --- import dashboardObj from './dashboard.devdocs.json'; diff --git a/api_docs/dashboard_enhanced.mdx b/api_docs/dashboard_enhanced.mdx index 7e58cc9e1a86a4..6bb136c589eb9a 100644 --- a/api_docs/dashboard_enhanced.mdx +++ b/api_docs/dashboard_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dashboardEnhanced title: "dashboardEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the dashboardEnhanced plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboardEnhanced'] --- import dashboardEnhancedObj from './dashboard_enhanced.devdocs.json'; diff --git a/api_docs/data.devdocs.json b/api_docs/data.devdocs.json index bd569dc580afc5..fee7fb3cd9e2be 100644 --- a/api_docs/data.devdocs.json +++ b/api_docs/data.devdocs.json @@ -7334,181 +7334,6 @@ ], "initialIsOpen": false }, - { - "parentPluginId": "data", - "id": "def-public.IDataPluginServices", - "type": "Interface", - "tags": [], - "label": "IDataPluginServices", - "description": [], - "signature": [ - { - "pluginId": "data", - "scope": "public", - "docId": "kibDataPluginApi", - "section": "def-public.IDataPluginServices", - "text": "IDataPluginServices" - }, - " extends Partial<", - { - "pluginId": "core", - "scope": "public", - "docId": "kibCorePluginApi", - "section": "def-public.CoreStart", - "text": "CoreStart" - }, - ">" - ], - "path": "src/plugins/data/public/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "data", - "id": "def-public.IDataPluginServices.appName", - "type": "string", - "tags": [], - "label": "appName", - "description": [], - "path": "src/plugins/data/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "data", - "id": "def-public.IDataPluginServices.uiSettings", - "type": "Object", - "tags": [], - "label": "uiSettings", - "description": [], - "signature": [ - "IUiSettingsClient" - ], - "path": "src/plugins/data/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "data", - "id": "def-public.IDataPluginServices.savedObjects", - "type": "Object", - "tags": [], - "label": "savedObjects", - "description": [], - "signature": [ - "SavedObjectsStart" - ], - "path": "src/plugins/data/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "data", - "id": "def-public.IDataPluginServices.notifications", - "type": "Object", - "tags": [], - "label": "notifications", - "description": [], - "signature": [ - "NotificationsStart" - ], - "path": "src/plugins/data/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "data", - "id": "def-public.IDataPluginServices.application", - "type": "Object", - "tags": [], - "label": "application", - "description": [], - "signature": [ - "ApplicationStart" - ], - "path": "src/plugins/data/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "data", - "id": "def-public.IDataPluginServices.http", - "type": "Object", - "tags": [], - "label": "http", - "description": [], - "signature": [ - "HttpSetup" - ], - "path": "src/plugins/data/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "data", - "id": "def-public.IDataPluginServices.storage", - "type": "Object", - "tags": [], - "label": "storage", - "description": [], - "signature": [ - { - "pluginId": "kibanaUtils", - "scope": "public", - "docId": "kibKibanaUtilsPluginApi", - "section": "def-public.IStorageWrapper", - "text": "IStorageWrapper" - }, - "" - ], - "path": "src/plugins/data/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "data", - "id": "def-public.IDataPluginServices.data", - "type": "Object", - "tags": [], - "label": "data", - "description": [], - "signature": [ - { - "pluginId": "data", - "scope": "public", - "docId": "kibDataPluginApi", - "section": "def-public.DataPublicPluginStart", - "text": "DataPublicPluginStart" - } - ], - "path": "src/plugins/data/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "data", - "id": "def-public.IDataPluginServices.usageCollection", - "type": "Object", - "tags": [], - "label": "usageCollection", - "description": [], - "signature": [ - { - "pluginId": "usageCollection", - "scope": "public", - "docId": "kibUsageCollectionPluginApi", - "section": "def-public.UsageCollectionStart", - "text": "UsageCollectionStart" - }, - " | undefined" - ], - "path": "src/plugins/data/public/types.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, { "parentPluginId": "data", "id": "def-public.IEsSearchRequest", @@ -7915,6 +7740,24 @@ "path": "src/plugins/data/common/search/types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "data", + "id": "def-public.ISearchOptions.transport", + "type": "Object", + "tags": [], + "label": "transport", + "description": [ + "\nTransportRequestOptions, other than `signal`, to pass through to the ES client.\nTo pass an abort signal, use {@link ISearchOptions.abortSignal}" + ], + "signature": [ + "Omit<", + "TransportRequestOptions", + ", \"signal\"> | undefined" + ], + "path": "src/plugins/data/common/search/types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -8234,7 +8077,7 @@ "tags": [], "label": "attributes", "description": [ - "{@inheritdoc SavedObjectAttributes}" + "The data for a Saved Object is stored as an object in the `attributes` property." ], "signature": [ "T" @@ -17173,6 +17016,24 @@ "path": "src/plugins/data/common/search/types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "data", + "id": "def-server.ISearchOptions.transport", + "type": "Object", + "tags": [], + "label": "transport", + "description": [ + "\nTransportRequestOptions, other than `signal`, to pass through to the ES client.\nTo pass an abort signal, use {@link ISearchOptions.abortSignal}" + ], + "signature": [ + "Omit<", + "TransportRequestOptions", + ", \"signal\"> | undefined" + ], + "path": "src/plugins/data/common/search/types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -20274,7 +20135,7 @@ "section": "def-common.RuntimeFieldSpec", "text": "RuntimeFieldSpec" }, - " | undefined; fixedInterval?: string[] | undefined; timeZone?: string[] | undefined; timeSeriesDimension?: boolean | undefined; timeSeriesMetric?: \"gauge\" | \"histogram\" | \"summary\" | \"counter\" | undefined; shortDotsEnable?: boolean | undefined; isMapped?: boolean | undefined; }" + " | undefined; fixedInterval?: string[] | undefined; timeZone?: string[] | undefined; timeSeriesDimension?: boolean | undefined; timeSeriesMetric?: \"gauge\" | \"histogram\" | \"summary\" | \"counter\" | undefined; shortDotsEnable?: boolean | undefined; isMapped?: boolean | undefined; parentName?: string | undefined; }" ], "path": "src/plugins/data_views/common/fields/data_view_field.ts", "deprecated": false, @@ -23303,83 +23164,6 @@ ], "initialIsOpen": false }, - { - "parentPluginId": "data", - "id": "def-common.FilterValueFormatter", - "type": "Interface", - "tags": [], - "label": "FilterValueFormatter", - "description": [], - "path": "src/plugins/data/common/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "data", - "id": "def-common.FilterValueFormatter.convert", - "type": "Function", - "tags": [], - "label": "convert", - "description": [], - "signature": [ - "(value: any) => string" - ], - "path": "src/plugins/data/common/types.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "data", - "id": "def-common.FilterValueFormatter.convert.$1", - "type": "Any", - "tags": [], - "label": "value", - "description": [], - "signature": [ - "any" - ], - "path": "src/plugins/data/common/types.ts", - "deprecated": false, - "trackAdoption": false - } - ] - }, - { - "parentPluginId": "data", - "id": "def-common.FilterValueFormatter.getConverterFor", - "type": "Function", - "tags": [], - "label": "getConverterFor", - "description": [], - "signature": [ - "(type: string) => FilterFormatterFunction" - ], - "path": "src/plugins/data/common/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "data", - "id": "def-common.FilterValueFormatter.getConverterFor.$1", - "type": "string", - "tags": [], - "label": "type", - "description": [], - "signature": [ - "string" - ], - "path": "src/plugins/data/common/types.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [] - } - ], - "initialIsOpen": false - }, { "parentPluginId": "data", "id": "def-common.GetFieldsOptions", @@ -23826,7 +23610,7 @@ "tags": [], "label": "attributes", "description": [ - "{@inheritdoc SavedObjectAttributes}" + "The data for a Saved Object is stored as an object in the `attributes` property." ], "signature": [ "T" @@ -24594,7 +24378,7 @@ "section": "def-common.RuntimeFieldSpec", "text": "RuntimeFieldSpec" }, - " | undefined; fixedInterval?: string[] | undefined; timeZone?: string[] | undefined; timeSeriesDimension?: boolean | undefined; timeSeriesMetric?: \"gauge\" | \"histogram\" | \"summary\" | \"counter\" | undefined; shortDotsEnable?: boolean | undefined; isMapped?: boolean | undefined; }" + " | undefined; fixedInterval?: string[] | undefined; timeZone?: string[] | undefined; timeSeriesDimension?: boolean | undefined; timeSeriesMetric?: \"gauge\" | \"histogram\" | \"summary\" | \"counter\" | undefined; shortDotsEnable?: boolean | undefined; isMapped?: boolean | undefined; parentName?: string | undefined; }" ], "path": "src/plugins/data_views/common/types.ts", "deprecated": false, diff --git a/api_docs/data.mdx b/api_docs/data.mdx index f7f878ec5cf745..3abcd1715a326e 100644 --- a/api_docs/data.mdx +++ b/api_docs/data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data title: "data" image: https://source.unsplash.com/400x175/?github description: API docs for the data plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data'] --- import dataObj from './data.devdocs.json'; @@ -21,7 +21,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 3144 | 34 | 2444 | 23 | +| 3132 | 33 | 2429 | 23 | ## Client diff --git a/api_docs/data_query.mdx b/api_docs/data_query.mdx index 5206f1ec467308..24769d05ff0d00 100644 --- a/api_docs/data_query.mdx +++ b/api_docs/data_query.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data-query title: "data.query" image: https://source.unsplash.com/400x175/?github description: API docs for the data.query plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.query'] --- import dataQueryObj from './data_query.devdocs.json'; @@ -21,7 +21,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 3144 | 34 | 2444 | 23 | +| 3132 | 33 | 2429 | 23 | ## Client diff --git a/api_docs/data_search.devdocs.json b/api_docs/data_search.devdocs.json index 399c9e5b7f1881..a4b47f09c07b1e 100644 --- a/api_docs/data_search.devdocs.json +++ b/api_docs/data_search.devdocs.json @@ -23757,7 +23757,9 @@ "parentPluginId": "data", "id": "def-common.EqlSearchStrategyRequest.options", "type": "Object", - "tags": [], + "tags": [ + "deprecated" + ], "label": "options", "description": [], "signature": [ @@ -23765,8 +23767,14 @@ " | undefined" ], "path": "src/plugins/data/common/search/strategies/eql_search/types.ts", - "deprecated": false, - "trackAdoption": false + "deprecated": true, + "trackAdoption": false, + "references": [ + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/public/common/hooks/eql/api.ts" + } + ] } ], "initialIsOpen": false @@ -25326,6 +25334,24 @@ "path": "src/plugins/data/common/search/types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "data", + "id": "def-common.ISearchOptions.transport", + "type": "Object", + "tags": [], + "label": "transport", + "description": [ + "\nTransportRequestOptions, other than `signal`, to pass through to the ES client.\nTo pass an abort signal, use {@link ISearchOptions.abortSignal}" + ], + "signature": [ + "Omit<", + "TransportRequestOptions", + ", \"signal\"> | undefined" + ], + "path": "src/plugins/data/common/search/types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/data_search.mdx b/api_docs/data_search.mdx index 948da55a39f746..f3760e333478ad 100644 --- a/api_docs/data_search.mdx +++ b/api_docs/data_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data-search title: "data.search" image: https://source.unsplash.com/400x175/?github description: API docs for the data.search plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.search'] --- import dataSearchObj from './data_search.devdocs.json'; @@ -21,7 +21,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 3144 | 34 | 2444 | 23 | +| 3132 | 33 | 2429 | 23 | ## Client diff --git a/api_docs/data_view_editor.mdx b/api_docs/data_view_editor.mdx index 95fa4182594f84..d80e42fa102772 100644 --- a/api_docs/data_view_editor.mdx +++ b/api_docs/data_view_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewEditor title: "dataViewEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewEditor plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewEditor'] --- import dataViewEditorObj from './data_view_editor.devdocs.json'; diff --git a/api_docs/data_view_field_editor.devdocs.json b/api_docs/data_view_field_editor.devdocs.json index 7843f117d57e9e..50c48bbe6aed25 100644 --- a/api_docs/data_view_field_editor.devdocs.json +++ b/api_docs/data_view_field_editor.devdocs.json @@ -26,7 +26,13 @@ "text": "FormatEditorProps" }, "

, ", - "FormatEditorState", + { + "pluginId": "dataViewFieldEditor", + "scope": "public", + "docId": "kibDataViewFieldEditorPluginApi", + "section": "def-public.FormatEditorState", + "text": "FormatEditorState" + }, " & S, any>" ], "path": "src/plugins/data_view_field_editor/public/components/field_format_editor/editors/default/default.tsx", @@ -52,7 +58,13 @@ "label": "state", "description": [], "signature": [ - "FormatEditorState", + { + "pluginId": "dataViewFieldEditor", + "scope": "public", + "docId": "kibDataViewFieldEditorPluginApi", + "section": "def-public.FormatEditorState", + "text": "FormatEditorState" + }, " & S" ], "path": "src/plugins/data_view_field_editor/public/components/field_format_editor/editors/default/default.tsx", @@ -76,9 +88,21 @@ "text": "FormatEditorProps" }, "<{}>, state: ", - "FormatEditorState", + { + "pluginId": "dataViewFieldEditor", + "scope": "public", + "docId": "kibDataViewFieldEditorPluginApi", + "section": "def-public.FormatEditorState", + "text": "FormatEditorState" + }, ") => { error: string | undefined; samples: ", - "Sample", + { + "pluginId": "dataViewFieldEditor", + "scope": "public", + "docId": "kibDataViewFieldEditorPluginApi", + "section": "def-public.Sample", + "text": "Sample" + }, "[]; }" ], "path": "src/plugins/data_view_field_editor/public/components/field_format_editor/editors/default/default.tsx", @@ -115,7 +139,13 @@ "label": "state", "description": [], "signature": [ - "FormatEditorState" + { + "pluginId": "dataViewFieldEditor", + "scope": "public", + "docId": "kibDataViewFieldEditorPluginApi", + "section": "def-public.FormatEditorState", + "text": "FormatEditorState" + } ], "path": "src/plugins/data_view_field_editor/public/components/field_format_editor/editors/default/default.tsx", "deprecated": false, @@ -175,6 +205,64 @@ } ], "initialIsOpen": false + }, + { + "parentPluginId": "dataViewFieldEditor", + "id": "def-public.DeleteCompositeSubfield", + "type": "Class", + "tags": [], + "label": "DeleteCompositeSubfield", + "description": [ + "\nError throw when there's an attempt to directly delete a composite subfield" + ], + "signature": [ + { + "pluginId": "dataViewFieldEditor", + "scope": "public", + "docId": "kibDataViewFieldEditorPluginApi", + "section": "def-public.DeleteCompositeSubfield", + "text": "DeleteCompositeSubfield" + }, + " extends Error" + ], + "path": "src/plugins/data_view_field_editor/public/open_delete_modal.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "dataViewFieldEditor", + "id": "def-public.DeleteCompositeSubfield.Unnamed", + "type": "Function", + "tags": [], + "label": "Constructor", + "description": [], + "signature": [ + "any" + ], + "path": "src/plugins/data_view_field_editor/public/open_delete_modal.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "dataViewFieldEditor", + "id": "def-public.DeleteCompositeSubfield.Unnamed.$1", + "type": "string", + "tags": [], + "label": "fieldName", + "description": [], + "signature": [ + "string" + ], + "path": "src/plugins/data_view_field_editor/public/open_delete_modal.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false } ], "functions": [], @@ -188,6 +276,23 @@ "description": [ "\nThe data model for the field editor" ], + "signature": [ + { + "pluginId": "dataViewFieldEditor", + "scope": "public", + "docId": "kibDataViewFieldEditorPluginApi", + "section": "def-public.Field", + "text": "Field" + }, + " extends ", + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.RuntimeField", + "text": "RuntimeField" + } + ], "path": "src/plugins/data_view_field_editor/public/types.ts", "deprecated": false, "trackAdoption": false, @@ -207,44 +312,12 @@ }, { "parentPluginId": "dataViewFieldEditor", - "id": "def-public.Field.type", - "type": "CompoundType", - "tags": [], - "label": "type", - "description": [ - "\nES type" - ], - "signature": [ - "\"boolean\" | \"date\" | \"keyword\" | \"ip\" | \"geo_point\" | \"long\" | \"double\" | \"composite\"" - ], - "path": "src/plugins/data_view_field_editor/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "dataViewFieldEditor", - "id": "def-public.Field.script", - "type": "Object", - "tags": [], - "label": "script", - "description": [ - "\nsource of the runtime field script" - ], - "signature": [ - "{ source: string; } | undefined" - ], - "path": "src/plugins/data_view_field_editor/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "dataViewFieldEditor", - "id": "def-public.Field.customLabel", + "id": "def-public.Field.parentName", "type": "string", "tags": [], - "label": "customLabel", + "label": "parentName", "description": [ - "\ncustom label for display" + "\nName of parent field. Used for composite subfields" ], "signature": [ "string | undefined" @@ -252,47 +325,6 @@ "path": "src/plugins/data_view_field_editor/public/types.ts", "deprecated": false, "trackAdoption": false - }, - { - "parentPluginId": "dataViewFieldEditor", - "id": "def-public.Field.popularity", - "type": "number", - "tags": [], - "label": "popularity", - "description": [ - "\ncustom popularity" - ], - "signature": [ - "number | undefined" - ], - "path": "src/plugins/data_view_field_editor/public/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "dataViewFieldEditor", - "id": "def-public.Field.format", - "type": "Object", - "tags": [], - "label": "format", - "description": [ - "\nconfiguration of the field format" - ], - "signature": [ - { - "pluginId": "fieldFormats", - "scope": "common", - "docId": "kibFieldFormatsPluginApi", - "section": "def-common.SerializedFieldFormat", - "text": "SerializedFieldFormat" - }, - "<{}, ", - "SerializableRecord", - "> | undefined" - ], - "path": "src/plugins/data_view_field_editor/public/types.ts", - "deprecated": false, - "trackAdoption": false } ], "initialIsOpen": false @@ -433,13 +465,65 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "dataViewFieldEditor", + "id": "def-public.FormatEditorState", + "type": "Interface", + "tags": [], + "label": "FormatEditorState", + "description": [], + "path": "src/plugins/data_view_field_editor/public/components/field_format_editor/format_editor.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "dataViewFieldEditor", + "id": "def-public.FormatEditorState.EditorComponent", + "type": "CompoundType", + "tags": [], + "label": "EditorComponent", + "description": [], + "signature": [ + "React.LazyExoticComponent<", + { + "pluginId": "dataViewFieldEditor", + "scope": "public", + "docId": "kibDataViewFieldEditorPluginApi", + "section": "def-public.FieldFormatEditor", + "text": "FieldFormatEditor" + }, + "<{}>> | null" + ], + "path": "src/plugins/data_view_field_editor/public/components/field_format_editor/format_editor.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "dataViewFieldEditor", + "id": "def-public.FormatEditorState.fieldFormatId", + "type": "string", + "tags": [], + "label": "fieldFormatId", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/data_view_field_editor/public/components/field_format_editor/format_editor.tsx", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "dataViewFieldEditor", "id": "def-public.OpenFieldDeleteModalOptions", "type": "Interface", "tags": [], "label": "OpenFieldDeleteModalOptions", - "description": [], + "description": [ + "\nOptions for opening the field editor" + ], "path": "src/plugins/data_view_field_editor/public/open_delete_modal.tsx", "deprecated": false, "trackAdoption": false, @@ -450,7 +534,9 @@ "type": "Object", "tags": [], "label": "ctx", - "description": [], + "description": [ + "\nConfig for the delete modal" + ], "signature": [ "{ dataView: ", { @@ -472,7 +558,9 @@ "type": "Function", "tags": [], "label": "onDelete", - "description": [], + "description": [ + "\nCallback fired when fields are deleted" + ], "signature": [ "((fieldNames: string[]) => void) | undefined" ], @@ -486,7 +574,9 @@ "type": "Array", "tags": [], "label": "fieldNames", - "description": [], + "description": [ + "- the names of the deleted fields" + ], "signature": [ "string[]" ], @@ -504,7 +594,9 @@ "type": "CompoundType", "tags": [], "label": "fieldName", - "description": [], + "description": [ + "\nNames of the fields to be deleted" + ], "signature": [ "string | string[]" ], @@ -570,7 +662,7 @@ "section": "def-common.DataViewField", "text": "DataViewField" }, - ") => void) | undefined" + "[]) => void) | undefined" ], "path": "src/plugins/data_view_field_editor/public/open_editor.tsx", "deprecated": false, @@ -579,10 +671,12 @@ { "parentPluginId": "dataViewFieldEditor", "id": "def-public.OpenFieldEditorOptions.onSave.$1", - "type": "Object", + "type": "Array", "tags": [], "label": "field", - "description": [], + "description": [ + "- the fields that were saved" + ], "signature": [ { "pluginId": "dataViews", @@ -590,7 +684,8 @@ "docId": "kibDataViewsPluginApi", "section": "def-common.DataViewField", "text": "DataViewField" - } + }, + "[]" ], "path": "src/plugins/data_view_field_editor/public/open_editor.tsx", "deprecated": false, @@ -641,6 +736,149 @@ } ], "initialIsOpen": false + }, + { + "parentPluginId": "dataViewFieldEditor", + "id": "def-public.Props", + "type": "Interface", + "tags": [], + "label": "Props", + "description": [], + "path": "src/plugins/data_view_field_editor/public/components/delete_field_provider.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "dataViewFieldEditor", + "id": "def-public.Props.children", + "type": "Function", + "tags": [], + "label": "children", + "description": [], + "signature": [ + "(deleteFieldHandler: DeleteFieldFunc) => React.ReactNode" + ], + "path": "src/plugins/data_view_field_editor/public/components/delete_field_provider.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "dataViewFieldEditor", + "id": "def-public.Props.children.$1", + "type": "Function", + "tags": [], + "label": "deleteFieldHandler", + "description": [], + "signature": [ + "DeleteFieldFunc" + ], + "path": "src/plugins/data_view_field_editor/public/components/delete_field_provider.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "dataViewFieldEditor", + "id": "def-public.Props.dataView", + "type": "Object", + "tags": [], + "label": "dataView", + "description": [ + "\nData view of fields to be deleted" + ], + "signature": [ + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.DataView", + "text": "DataView" + } + ], + "path": "src/plugins/data_view_field_editor/public/components/delete_field_provider.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "dataViewFieldEditor", + "id": "def-public.Props.onDelete", + "type": "Function", + "tags": [], + "label": "onDelete", + "description": [ + "\nCallback fired when fields are deleted" + ], + "signature": [ + "((fieldNames: string[]) => void) | undefined" + ], + "path": "src/plugins/data_view_field_editor/public/components/delete_field_provider.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "dataViewFieldEditor", + "id": "def-public.Props.onDelete.$1", + "type": "Array", + "tags": [], + "label": "fieldNames", + "description": [ + "- the names of the deleted fields" + ], + "signature": [ + "string[]" + ], + "path": "src/plugins/data_view_field_editor/public/components/delete_field_provider.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "dataViewFieldEditor", + "id": "def-public.Sample", + "type": "Interface", + "tags": [], + "label": "Sample", + "description": [], + "path": "src/plugins/data_view_field_editor/public/components/field_format_editor/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "dataViewFieldEditor", + "id": "def-public.Sample.input", + "type": "CompoundType", + "tags": [], + "label": "input", + "description": [], + "signature": [ + "object | React.ReactText | React.ReactText[] | Record" + ], + "path": "src/plugins/data_view_field_editor/public/components/field_format_editor/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "dataViewFieldEditor", + "id": "def-public.Sample.output", + "type": "string", + "tags": [], + "label": "output", + "description": [], + "path": "src/plugins/data_view_field_editor/public/components/field_format_editor/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false } ], "enums": [], @@ -758,7 +996,7 @@ "tags": [], "label": "openEditor", "description": [ - "\nmethod to open the data view field editor fly-out" + "\nMethod to open the data view field editor fly-out" ], "signature": [ "(options: ", @@ -805,7 +1043,9 @@ "type": "Function", "tags": [], "label": "openDeleteModal", - "description": [], + "description": [ + "\nMethod to open the data view field delete fly-out" + ], "signature": [ "(options: ", { @@ -827,7 +1067,9 @@ "type": "Object", "tags": [], "label": "options", - "description": [], + "description": [ + "Configuration options for the fly-out" + ], "signature": [ { "pluginId": "dataViewFieldEditor", @@ -881,7 +1123,9 @@ "type": "Object", "tags": [], "label": "userPermissions", - "description": [], + "description": [ + "\nConvenience method for user permissions checks" + ], "signature": [ "{ editIndexPattern: () => boolean; }" ], @@ -895,10 +1139,18 @@ "type": "Function", "tags": [], "label": "DeleteRuntimeFieldProvider", - "description": [], + "description": [ + "\nContext provider for delete runtime field modal" + ], "signature": [ "React.FunctionComponent<", - "Props", + { + "pluginId": "dataViewFieldEditor", + "scope": "public", + "docId": "kibDataViewFieldEditorPluginApi", + "section": "def-public.Props", + "text": "Props" + }, ">" ], "path": "src/plugins/data_view_field_editor/public/types.ts", diff --git a/api_docs/data_view_field_editor.mdx b/api_docs/data_view_field_editor.mdx index 19de55351a8de5..c9d493d03d3ead 100644 --- a/api_docs/data_view_field_editor.mdx +++ b/api_docs/data_view_field_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewFieldEditor title: "dataViewFieldEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewFieldEditor plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewFieldEditor'] --- import dataViewFieldEditorObj from './data_view_field_editor.devdocs.json'; @@ -21,7 +21,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 49 | 0 | 29 | 3 | +| 60 | 0 | 30 | 0 | ## Client diff --git a/api_docs/data_view_management.mdx b/api_docs/data_view_management.mdx index b1851139b7d9e3..91629eeb027f04 100644 --- a/api_docs/data_view_management.mdx +++ b/api_docs/data_view_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewManagement title: "dataViewManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewManagement plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewManagement'] --- import dataViewManagementObj from './data_view_management.devdocs.json'; diff --git a/api_docs/data_views.devdocs.json b/api_docs/data_views.devdocs.json index 389500b91049cd..8766b1f50d97db 100644 --- a/api_docs/data_views.devdocs.json +++ b/api_docs/data_views.devdocs.json @@ -1705,7 +1705,7 @@ "section": "def-common.RuntimeFieldSpec", "text": "RuntimeFieldSpec" }, - " | undefined; fixedInterval?: string[] | undefined; timeZone?: string[] | undefined; timeSeriesDimension?: boolean | undefined; timeSeriesMetric?: \"gauge\" | \"histogram\" | \"summary\" | \"counter\" | undefined; shortDotsEnable?: boolean | undefined; isMapped?: boolean | undefined; }" + " | undefined; fixedInterval?: string[] | undefined; timeZone?: string[] | undefined; timeSeriesDimension?: boolean | undefined; timeSeriesMetric?: \"gauge\" | \"histogram\" | \"summary\" | \"counter\" | undefined; shortDotsEnable?: boolean | undefined; isMapped?: boolean | undefined; parentName?: string | undefined; }" ], "path": "src/plugins/data_views/common/fields/data_view_field.ts", "deprecated": false, @@ -5936,6 +5936,70 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "dataViews", + "id": "def-public.RuntimeField", + "type": "Interface", + "tags": [], + "label": "RuntimeField", + "description": [ + "\nThis is the RuntimeField interface enhanced with Data view field\nconfiguration: field format definition, customLabel or popularity." + ], + "signature": [ + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.RuntimeField", + "text": "RuntimeField" + }, + " extends ", + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.RuntimeFieldBase", + "text": "RuntimeFieldBase" + }, + ",", + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.FieldConfiguration", + "text": "FieldConfiguration" + } + ], + "path": "src/plugins/data_views/common/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "dataViews", + "id": "def-public.RuntimeField.fields", + "type": "Object", + "tags": [], + "label": "fields", + "description": [ + "\nSubfields of composite field" + ], + "signature": [ + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.RuntimeFieldSubFields", + "text": "RuntimeFieldSubFields" + }, + " | undefined" + ], + "path": "src/plugins/data_views/common/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "dataViews", "id": "def-public.SavedObjectsClientCommon", @@ -6454,7 +6518,7 @@ "section": "def-common.RuntimeFieldSpec", "text": "RuntimeFieldSpec" }, - " | undefined; fixedInterval?: string[] | undefined; timeZone?: string[] | undefined; timeSeriesDimension?: boolean | undefined; timeSeriesMetric?: \"gauge\" | \"histogram\" | \"summary\" | \"counter\" | undefined; shortDotsEnable?: boolean | undefined; isMapped?: boolean | undefined; }" + " | undefined; fixedInterval?: string[] | undefined; timeZone?: string[] | undefined; timeSeriesDimension?: boolean | undefined; timeSeriesMetric?: \"gauge\" | \"histogram\" | \"summary\" | \"counter\" | undefined; shortDotsEnable?: boolean | undefined; isMapped?: boolean | undefined; parentName?: string | undefined; }" ], "path": "src/plugins/data_views/common/types.ts", "deprecated": false, @@ -6536,7 +6600,7 @@ "tags": [], "label": "RuntimeType", "description": [ - "\nRuntime field - type of value returned" + "\nRuntime field types" ], "signature": [ "\"boolean\" | \"date\" | \"keyword\" | \"ip\" | \"geo_point\" | \"long\" | \"double\" | \"composite\"" @@ -10667,7 +10731,9 @@ "\nA record of capabilities (aggregations) for an index rollup job" ], "signature": [ - "[index: string]: { aggs?: _.Dictionary<", + "[index: string]: { aggs?: ", + "Dictionary", + "<", { "pluginId": "dataViews", "scope": "common", @@ -11114,7 +11180,7 @@ "section": "def-common.RuntimeFieldSpec", "text": "RuntimeFieldSpec" }, - " | undefined; fixedInterval?: string[] | undefined; timeZone?: string[] | undefined; timeSeriesDimension?: boolean | undefined; timeSeriesMetric?: \"gauge\" | \"histogram\" | \"summary\" | \"counter\" | undefined; shortDotsEnable?: boolean | undefined; isMapped?: boolean | undefined; }" + " | undefined; fixedInterval?: string[] | undefined; timeZone?: string[] | undefined; timeSeriesDimension?: boolean | undefined; timeSeriesMetric?: \"gauge\" | \"histogram\" | \"summary\" | \"counter\" | undefined; shortDotsEnable?: boolean | undefined; isMapped?: boolean | undefined; parentName?: string | undefined; }" ], "path": "src/plugins/data_views/common/types.ts", "deprecated": false, @@ -14365,7 +14431,7 @@ "section": "def-common.RuntimeFieldSpec", "text": "RuntimeFieldSpec" }, - " | undefined; fixedInterval?: string[] | undefined; timeZone?: string[] | undefined; timeSeriesDimension?: boolean | undefined; timeSeriesMetric?: \"gauge\" | \"histogram\" | \"summary\" | \"counter\" | undefined; shortDotsEnable?: boolean | undefined; isMapped?: boolean | undefined; }" + " | undefined; fixedInterval?: string[] | undefined; timeZone?: string[] | undefined; timeSeriesDimension?: boolean | undefined; timeSeriesMetric?: \"gauge\" | \"histogram\" | \"summary\" | \"counter\" | undefined; shortDotsEnable?: boolean | undefined; isMapped?: boolean | undefined; parentName?: string | undefined; }" ], "path": "src/plugins/data_views/common/fields/data_view_field.ts", "deprecated": false, @@ -19607,15 +19673,14 @@ "\nSubfields of composite field" ], "signature": [ - "Record | undefined" + " | undefined" ], "path": "src/plugins/data_views/common/types.ts", "deprecated": false, @@ -19660,9 +19725,7 @@ "type": "CompoundType", "tags": [], "label": "type", - "description": [ - "\nType of runtime field, can only be primitive type" - ], + "description": [], "signature": [ "\"boolean\" | \"date\" | \"keyword\" | \"ip\" | \"geo_point\" | \"long\" | \"double\"" ], @@ -19768,7 +19831,7 @@ "tags": [], "label": "attributes", "description": [ - "{@inheritdoc SavedObjectAttributes}" + "The data for a Saved Object is stored as an object in the `attributes` property." ], "signature": [ "T" @@ -21094,7 +21157,7 @@ "section": "def-common.RuntimeFieldSpec", "text": "RuntimeFieldSpec" }, - " | undefined; fixedInterval?: string[] | undefined; timeZone?: string[] | undefined; timeSeriesDimension?: boolean | undefined; timeSeriesMetric?: \"gauge\" | \"histogram\" | \"summary\" | \"counter\" | undefined; shortDotsEnable?: boolean | undefined; isMapped?: boolean | undefined; }" + " | undefined; fixedInterval?: string[] | undefined; timeZone?: string[] | undefined; timeSeriesDimension?: boolean | undefined; timeSeriesMetric?: \"gauge\" | \"histogram\" | \"summary\" | \"counter\" | undefined; shortDotsEnable?: boolean | undefined; isMapped?: boolean | undefined; parentName?: string | undefined; }" ], "path": "src/plugins/data_views/common/types.ts", "deprecated": false, @@ -21336,8 +21399,8 @@ "pluginId": "dataViews", "scope": "common", "docId": "kibDataViewsPluginApi", - "section": "def-common.RuntimeTypeExceptComposite", - "text": "RuntimeTypeExceptComposite" + "section": "def-common.RuntimePrimitiveTypes", + "text": "RuntimePrimitiveTypes" }, "; }> | undefined; }" ], @@ -21348,15 +21411,38 @@ }, { "parentPluginId": "dataViews", - "id": "def-common.RuntimeType", + "id": "def-common.RuntimeFieldSubFields", "type": "Type", "tags": [], - "label": "RuntimeType", + "label": "RuntimeFieldSubFields", + "description": [], + "signature": [ + "{ [x: string]: ", + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.RuntimeFieldSubField", + "text": "RuntimeFieldSubField" + }, + "; }" + ], + "path": "src/plugins/data_views/common/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "dataViews", + "id": "def-common.RuntimePrimitiveTypes", + "type": "Type", + "tags": [], + "label": "RuntimePrimitiveTypes", "description": [ - "\nRuntime field - type of value returned" + "\nRuntime field primitive types - excluding composite" ], "signature": [ - "\"boolean\" | \"date\" | \"keyword\" | \"ip\" | \"geo_point\" | \"long\" | \"double\" | \"composite\"" + "\"boolean\" | \"date\" | \"keyword\" | \"ip\" | \"geo_point\" | \"long\" | \"double\"" ], "path": "src/plugins/data_views/common/types.ts", "deprecated": false, @@ -21365,15 +21451,15 @@ }, { "parentPluginId": "dataViews", - "id": "def-common.RuntimeTypeExceptComposite", + "id": "def-common.RuntimeType", "type": "Type", "tags": [], - "label": "RuntimeTypeExceptComposite", + "label": "RuntimeType", "description": [ - "\nPrimitive runtime field types" + "\nRuntime field types" ], "signature": [ - "\"boolean\" | \"date\" | \"keyword\" | \"ip\" | \"geo_point\" | \"long\" | \"double\"" + "\"boolean\" | \"date\" | \"keyword\" | \"ip\" | \"geo_point\" | \"long\" | \"double\" | \"composite\"" ], "path": "src/plugins/data_views/common/types.ts", "deprecated": false, diff --git a/api_docs/data_views.mdx b/api_docs/data_views.mdx index 892d584ba199e2..d51e59aad26652 100644 --- a/api_docs/data_views.mdx +++ b/api_docs/data_views.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViews title: "dataViews" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViews plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViews'] --- import dataViewsObj from './data_views.devdocs.json'; @@ -21,7 +21,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 963 | 0 | 206 | 1 | +| 966 | 0 | 208 | 1 | ## Client diff --git a/api_docs/data_visualizer.mdx b/api_docs/data_visualizer.mdx index 2dd8caad2c6552..7e5604ba906f3c 100644 --- a/api_docs/data_visualizer.mdx +++ b/api_docs/data_visualizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataVisualizer title: "dataVisualizer" image: https://source.unsplash.com/400x175/?github description: API docs for the dataVisualizer plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataVisualizer'] --- import dataVisualizerObj from './data_visualizer.devdocs.json'; diff --git a/api_docs/deprecations_by_api.mdx b/api_docs/deprecations_by_api.mdx index df2ac0366eadfe..cbcdfa18d87854 100644 --- a/api_docs/deprecations_by_api.mdx +++ b/api_docs/deprecations_by_api.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsByApi slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-api title: Deprecated API usage by API description: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -29,6 +29,9 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | stackAlerts, alerting, securitySolution, inputControlVis | - | | | stackAlerts, alerting, securitySolution, inputControlVis | - | | | actions, alerting | - | +| | visualizations, dashboard, ml, actions, alerting, canvas, enterpriseSearch, securitySolution, taskManager, savedSearch, savedObjects, embeddable, fleet, infra, graph, @kbn/core-saved-objects-server-internal | - | +| | visualizations, dashboard, ml, actions, alerting, canvas, enterpriseSearch, securitySolution, taskManager, savedSearch, savedObjects, embeddable, fleet, infra, graph, @kbn/core-saved-objects-server-internal | - | +| | securitySolution | - | | | encryptedSavedObjects, actions, data, cloud, ml, logstash, securitySolution | - | | | dashboard, stackAlerts, expressionPartitionVis | - | | | dashboard | - | @@ -47,7 +50,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | dataViewManagement | - | | | dataViewManagement | - | | | lens, observability, dataVisualizer, fleet, cloudSecurityPosture, discoverEnhanced, osquery, synthetics | - | -| | esUiShared, home, data, spaces, savedObjectsManagement, fleet, observability, ml, apm, indexLifecycleManagement, synthetics, upgradeAssistant, ux, kibanaOverview | - | +| | home, data, esUiShared, spaces, savedObjectsManagement, fleet, observability, ml, apm, indexLifecycleManagement, synthetics, upgradeAssistant, ux, kibanaOverview | - | | | spaces, ml, canvas, osquery | - | | | canvas | - | | | canvas | - | @@ -74,7 +77,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | savedObjectsTaggingOss, dashboard | 8.8.0 | | | dashboard | 8.8.0 | | | maps, dashboard, @kbn/core-saved-objects-migration-server-internal | 8.8.0 | -| | monitoring, kibanaUsageCollection, @kbn/core-metrics-server-internal, @kbn/core-usage-data-server-internal | 8.8.0 | +| | monitoring, kibanaUsageCollection, @kbn/core-apps-browser-internal, @kbn/core-metrics-server-internal, @kbn/core-status-server-internal, @kbn/core-usage-data-server-internal | 8.8.0 | | | security, fleet | 8.8.0 | | | security, fleet | 8.8.0 | | | security, fleet | 8.8.0 | @@ -147,6 +150,7 @@ Safe to remove. | | @kbn/core-injected-metadata-browser | | | @kbn/core-injected-metadata-browser | | | @kbn/core-metrics-server | +| | @kbn/core-saved-objects-common | | | @kbn/core-saved-objects-common | | | @kbn/core-saved-objects-server | | | @kbn/core-ui-settings-common | \ No newline at end of file diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx index 600739e4a9fdee..6d24e5724cf998 100644 --- a/api_docs/deprecations_by_plugin.mdx +++ b/api_docs/deprecations_by_plugin.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsByPlugin slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-plugin title: Deprecated API usage by plugin description: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -31,6 +31,14 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] +## @kbn/core-apps-browser-internal + +| Deprecated API | Reference location(s) | Remove By | +| ---------------|-----------|-----------| +| | [load_status.ts](https://github.com/elastic/kibana/tree/main/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts#:~:text=process), [load_status.ts](https://github.com/elastic/kibana/tree/main/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts#:~:text=process), [load_status.ts](https://github.com/elastic/kibana/tree/main/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts#:~:text=process), [load_status.ts](https://github.com/elastic/kibana/tree/main/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts#:~:text=process), [load_status.ts](https://github.com/elastic/kibana/tree/main/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts#:~:text=process), [load_status.ts](https://github.com/elastic/kibana/tree/main/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts#:~:text=process), [load_status.test.ts](https://github.com/elastic/kibana/tree/main/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts#:~:text=process) | 8.8.0 | + + + ## @kbn/core-elasticsearch-server-internal | Deprecated API | Reference location(s) | Remove By | @@ -70,6 +78,23 @@ so TS and code-reference navigation might not highlight them. | +## @kbn/core-saved-objects-server-internal + +| Deprecated API | Reference location(s) | Remove By | +| ---------------|-----------|-----------| +| | [collect_references_deep.test.ts](https://github.com/elastic/kibana/tree/main/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/collect_references_deep.test.ts#:~:text=SavedObjectAttributes), [collect_references_deep.test.ts](https://github.com/elastic/kibana/tree/main/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/collect_references_deep.test.ts#:~:text=SavedObjectAttributes) | - | +| | [collect_references_deep.test.ts](https://github.com/elastic/kibana/tree/main/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/collect_references_deep.test.ts#:~:text=SavedObjectAttributes), [collect_references_deep.test.ts](https://github.com/elastic/kibana/tree/main/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/collect_references_deep.test.ts#:~:text=SavedObjectAttributes) | - | + + + +## @kbn/core-status-server-internal + +| Deprecated API | Reference location(s) | Remove By | +| ---------------|-----------|-----------| +| | [status.ts](https://github.com/elastic/kibana/tree/main/packages/core/status/core-status-server-internal/src/routes/status.ts#:~:text=process), [status.ts](https://github.com/elastic/kibana/tree/main/packages/core/status/core-status-server-internal/src/routes/status.ts#:~:text=process) | 8.8.0 | + + + ## @kbn/core-usage-data-server-internal | Deprecated API | Reference location(s) | Remove By | @@ -86,6 +111,8 @@ so TS and code-reference navigation might not highlight them. | | | [plugin.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/plugin.ts#:~:text=authc) | - | | | [plugin.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/plugin.ts#:~:text=authz) | - | | | [plugin.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/plugin.ts#:~:text=index), [plugin.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/plugin.ts#:~:text=index) | - | +| | [actions_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/actions_client.ts#:~:text=SavedObjectAttributes), [actions_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/actions_client.ts#:~:text=SavedObjectAttributes), [actions_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/actions_client.ts#:~:text=SavedObjectAttributes), [actions_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/actions_client.ts#:~:text=SavedObjectAttributes), [actions_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/actions_client.ts#:~:text=SavedObjectAttributes), [actions_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/actions_client.ts#:~:text=SavedObjectAttributes), [actions_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/actions_client.ts#:~:text=SavedObjectAttributes), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/types.ts#:~:text=SavedObjectAttributes), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/types.ts#:~:text=SavedObjectAttributes), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/types.ts#:~:text=SavedObjectAttributes)+ 3 more | - | +| | [actions_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/actions_client.ts#:~:text=SavedObjectAttributes), [actions_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/actions_client.ts#:~:text=SavedObjectAttributes), [actions_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/actions_client.ts#:~:text=SavedObjectAttributes), [actions_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/actions_client.ts#:~:text=SavedObjectAttributes), [actions_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/actions_client.ts#:~:text=SavedObjectAttributes), [actions_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/actions_client.ts#:~:text=SavedObjectAttributes), [actions_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/actions_client.ts#:~:text=SavedObjectAttributes), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/types.ts#:~:text=SavedObjectAttributes), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/types.ts#:~:text=SavedObjectAttributes), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions/server/types.ts#:~:text=SavedObjectAttributes)+ 3 more | - | @@ -109,6 +136,8 @@ so TS and code-reference navigation might not highlight them. | | | [plugin.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/plugin.test.ts#:~:text=getKibanaFeatures) | 8.8.0 | | | [plugin.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/plugin.ts#:~:text=license%24), [license_state.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/license_state.test.ts#:~:text=license%24), [license_state.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/license_state.test.ts#:~:text=license%24) | 8.8.0 | | | [task.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/usage/task.ts#:~:text=index) | - | +| | [rule.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/common/rule.ts#:~:text=SavedObjectAttributes), [rule.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/common/rule.ts#:~:text=SavedObjectAttributes), [rule.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/common/rule.ts#:~:text=SavedObjectAttributes), [rule.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/common/rule.ts#:~:text=SavedObjectAttributes), [rule.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/common/rule.ts#:~:text=SavedObjectAttributes), [rule.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/common/rule.ts#:~:text=SavedObjectAttributes), [migrations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/saved_objects/geo_containment/migrations.ts#:~:text=SavedObjectAttributes), [migrations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/saved_objects/geo_containment/migrations.ts#:~:text=SavedObjectAttributes), [migrations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/saved_objects/migrations.ts#:~:text=SavedObjectAttributes), [migrations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/saved_objects/migrations.ts#:~:text=SavedObjectAttributes)+ 10 more | - | +| | [rule.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/common/rule.ts#:~:text=SavedObjectAttributes), [rule.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/common/rule.ts#:~:text=SavedObjectAttributes), [rule.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/common/rule.ts#:~:text=SavedObjectAttributes), [rule.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/common/rule.ts#:~:text=SavedObjectAttributes), [rule.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/common/rule.ts#:~:text=SavedObjectAttributes), [rule.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/common/rule.ts#:~:text=SavedObjectAttributes), [migrations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/saved_objects/geo_containment/migrations.ts#:~:text=SavedObjectAttributes), [migrations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/saved_objects/geo_containment/migrations.ts#:~:text=SavedObjectAttributes), [migrations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/saved_objects/migrations.ts#:~:text=SavedObjectAttributes), [migrations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/saved_objects/migrations.ts#:~:text=SavedObjectAttributes)+ 10 more | - | @@ -139,6 +168,8 @@ so TS and code-reference navigation might not highlight them. | | | [setup_expressions.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/public/setup_expressions.ts#:~:text=getTypes), [application.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/public/application.tsx#:~:text=getTypes), [functions.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/server/routes/functions/functions.ts#:~:text=getTypes) | - | | | [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts#:~:text=context), [embeddable.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts#:~:text=context), [esdocs.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/esdocs.ts#:~:text=context), [escount.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/escount.ts#:~:text=context), [filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/common/functions/filters.ts#:~:text=context), [neq.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/canvas_plugin_src/functions/common/neq.ts#:~:text=context), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts#:~:text=context) | - | | | [home.component.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/public/components/home/home.component.tsx#:~:text=KibanaPageTemplate), [home.component.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/public/components/home/home.component.tsx#:~:text=KibanaPageTemplate), [home.component.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/public/components/home/home.component.tsx#:~:text=KibanaPageTemplate) | - | +| | [find.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/server/routes/custom_elements/find.ts#:~:text=SavedObjectAttributes), [find.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/server/routes/custom_elements/find.ts#:~:text=SavedObjectAttributes), [find.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/server/routes/workpad/find.ts#:~:text=SavedObjectAttributes), [find.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/server/routes/workpad/find.ts#:~:text=SavedObjectAttributes), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/shareable_runtime/types.ts#:~:text=SavedObjectAttributes), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/shareable_runtime/types.ts#:~:text=SavedObjectAttributes) | - | +| | [find.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/server/routes/custom_elements/find.ts#:~:text=SavedObjectAttributes), [find.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/server/routes/custom_elements/find.ts#:~:text=SavedObjectAttributes), [find.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/server/routes/workpad/find.ts#:~:text=SavedObjectAttributes), [find.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/server/routes/workpad/find.ts#:~:text=SavedObjectAttributes), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/shareable_runtime/types.ts#:~:text=SavedObjectAttributes), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/canvas/shareable_runtime/types.ts#:~:text=SavedObjectAttributes) | - | @@ -186,6 +217,8 @@ so TS and code-reference navigation might not highlight them. | | | [saved_object_loader.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/services/saved_object_loader.ts#:~:text=SavedObject), [saved_object_loader.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/services/saved_object_loader.ts#:~:text=SavedObject), [saved_object_loader.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/services/saved_object_loader.ts#:~:text=SavedObject), [saved_objects.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/services/saved_objects.ts#:~:text=SavedObject), [saved_dashboard.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts#:~:text=SavedObject), [saved_dashboard.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts#:~:text=SavedObject), [dashboard_tagging.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/lib/dashboard_tagging.ts#:~:text=SavedObject), [dashboard_tagging.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/lib/dashboard_tagging.ts#:~:text=SavedObject), [clone_panel_action.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx#:~:text=SavedObject), [clone_panel_action.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx#:~:text=SavedObject)+ 1 more | 8.8.0 | | | [saved_dashboard.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts#:~:text=SavedObjectClass) | 8.8.0 | | | [types.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/types.ts#:~:text=onAppLeave), [dashboard_router.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/dashboard_router.tsx#:~:text=onAppLeave), [plugin.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/plugin.tsx#:~:text=onAppLeave) | 8.8.0 | +| | [saved_dashboard_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/common/saved_dashboard_references.ts#:~:text=SavedObjectAttributes), [saved_dashboard_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/common/saved_dashboard_references.ts#:~:text=SavedObjectAttributes), [saved_dashboard_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/common/saved_dashboard_references.ts#:~:text=SavedObjectAttributes), [saved_dashboard_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/common/saved_dashboard_references.ts#:~:text=SavedObjectAttributes), [saved_dashboard_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/common/saved_dashboard_references.ts#:~:text=SavedObjectAttributes), [saved_dashboard_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/common/saved_dashboard_references.ts#:~:text=SavedObjectAttributes), [saved_dashboard_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/common/saved_dashboard_references.ts#:~:text=SavedObjectAttributes), [saved_dashboard_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/common/saved_dashboard_references.ts#:~:text=SavedObjectAttributes), [saved_dashboard.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts#:~:text=SavedObjectAttributes), [saved_dashboard.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts#:~:text=SavedObjectAttributes)+ 8 more | - | +| | [saved_dashboard_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/common/saved_dashboard_references.ts#:~:text=SavedObjectAttributes), [saved_dashboard_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/common/saved_dashboard_references.ts#:~:text=SavedObjectAttributes), [saved_dashboard_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/common/saved_dashboard_references.ts#:~:text=SavedObjectAttributes), [saved_dashboard_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/common/saved_dashboard_references.ts#:~:text=SavedObjectAttributes), [saved_dashboard_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/common/saved_dashboard_references.ts#:~:text=SavedObjectAttributes), [saved_dashboard_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/common/saved_dashboard_references.ts#:~:text=SavedObjectAttributes), [saved_dashboard_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/common/saved_dashboard_references.ts#:~:text=SavedObjectAttributes), [saved_dashboard_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/common/saved_dashboard_references.ts#:~:text=SavedObjectAttributes), [saved_dashboard.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts#:~:text=SavedObjectAttributes), [saved_dashboard.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts#:~:text=SavedObjectAttributes)+ 8 more | - | | | [migrations_730.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/saved_objects/migrations_730.ts#:~:text=warning), [migrations_730.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/saved_objects/migrations_730.ts#:~:text=warning) | 8.8.0 | @@ -268,6 +301,8 @@ so TS and code-reference navigation might not highlight them. | | ---------------|-----------|-----------| | | [attribute_service.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/lib/attribute_service/attribute_service.tsx#:~:text=SavedObjectSaveModal), [attribute_service.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/lib/attribute_service/attribute_service.tsx#:~:text=SavedObjectSaveModal) | 8.8.0 | | | [container.test.ts](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/tests/container.test.ts#:~:text=executeTriggerActions), [container.test.ts](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/tests/container.test.ts#:~:text=executeTriggerActions), [container.test.ts](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/tests/container.test.ts#:~:text=executeTriggerActions), [container.test.ts](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/tests/container.test.ts#:~:text=executeTriggerActions), [explicit_input.test.ts](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/tests/explicit_input.test.ts#:~:text=executeTriggerActions) | - | +| | [add_panel_flyout.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx#:~:text=SavedObjectAttributes), [add_panel_flyout.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx#:~:text=SavedObjectAttributes), [add_panel_flyout.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx#:~:text=SavedObjectAttributes), [default_embeddable_factory_provider.ts](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts#:~:text=SavedObjectAttributes), [default_embeddable_factory_provider.ts](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts#:~:text=SavedObjectAttributes), [types.ts](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/types.ts#:~:text=SavedObjectAttributes), [types.ts](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/types.ts#:~:text=SavedObjectAttributes) | - | +| | [add_panel_flyout.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx#:~:text=SavedObjectAttributes), [add_panel_flyout.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx#:~:text=SavedObjectAttributes), [add_panel_flyout.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx#:~:text=SavedObjectAttributes), [default_embeddable_factory_provider.ts](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts#:~:text=SavedObjectAttributes), [default_embeddable_factory_provider.ts](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts#:~:text=SavedObjectAttributes), [types.ts](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/types.ts#:~:text=SavedObjectAttributes), [types.ts](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable/public/types.ts#:~:text=SavedObjectAttributes) | - | @@ -285,6 +320,8 @@ so TS and code-reference navigation might not highlight them. | | ---------------|-----------|-----------| | | [account_settings.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx#:~:text=uiApi), [account_settings.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx#:~:text=uiApi), [account_settings.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx#:~:text=uiApi), [account_settings.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx#:~:text=uiApi) | - | | | [check_access.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/enterprise_search/server/lib/check_access.ts#:~:text=authz), [check_access.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/enterprise_search/server/lib/check_access.ts#:~:text=authz), [check_access.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/enterprise_search/server/lib/check_access.ts#:~:text=authz) | - | +| | [telemetry.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts#:~:text=SavedObjectAttributes), [telemetry.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts#:~:text=SavedObjectAttributes), [telemetry.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts#:~:text=SavedObjectAttributes) | - | +| | [telemetry.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts#:~:text=SavedObjectAttributes), [telemetry.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts#:~:text=SavedObjectAttributes), [telemetry.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts#:~:text=SavedObjectAttributes) | - | @@ -314,6 +351,8 @@ so TS and code-reference navigation might not highlight them. | | | [plugin.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/fleet/server/plugin.ts#:~:text=disabled) | 8.8.0 | | | [plugin.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/fleet/server/plugin.ts#:~:text=disabled) | 8.8.0 | | | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/fleet/public/applications/integrations/index.tsx#:~:text=appBasePath) | 8.8.0 | +| | [epm.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/fleet/common/types/models/epm.ts#:~:text=SavedObjectAttributes), [epm.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/fleet/common/types/models/epm.ts#:~:text=SavedObjectAttributes), [settings.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/fleet/common/types/models/settings.ts#:~:text=SavedObjectAttributes), [settings.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/fleet/common/types/models/settings.ts#:~:text=SavedObjectAttributes) | - | +| | [epm.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/fleet/common/types/models/epm.ts#:~:text=SavedObjectAttributes), [epm.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/fleet/common/types/models/epm.ts#:~:text=SavedObjectAttributes), [settings.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/fleet/common/types/models/settings.ts#:~:text=SavedObjectAttributes), [settings.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/fleet/common/types/models/settings.ts#:~:text=SavedObjectAttributes) | - | @@ -327,6 +366,8 @@ so TS and code-reference navigation might not highlight them. | | | [deserialize.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/graph/public/services/persistence/deserialize.ts#:~:text=getNonScriptedFields), [datasource.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/graph/public/state_management/datasource.test.ts#:~:text=getNonScriptedFields), [deserialize.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/graph/public/services/persistence/deserialize.test.ts#:~:text=getNonScriptedFields), [deserialize.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/graph/public/services/persistence/deserialize.test.ts#:~:text=getNonScriptedFields) | - | | | [save_modal.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/graph/public/components/save_modal.tsx#:~:text=SavedObjectSaveModal), [save_modal.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/graph/public/components/save_modal.tsx#:~:text=SavedObjectSaveModal) | 8.8.0 | | | [plugin.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/graph/server/plugin.ts#:~:text=license%24) | 8.8.0 | +| | [saved_workspace_references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/graph/public/services/persistence/saved_workspace_references.ts#:~:text=SavedObjectAttributes), [saved_workspace_references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/graph/public/services/persistence/saved_workspace_references.ts#:~:text=SavedObjectAttributes), [saved_workspace_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts#:~:text=SavedObjectAttributes), [saved_workspace_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts#:~:text=SavedObjectAttributes) | - | +| | [saved_workspace_references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/graph/public/services/persistence/saved_workspace_references.ts#:~:text=SavedObjectAttributes), [saved_workspace_references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/graph/public/services/persistence/saved_workspace_references.ts#:~:text=SavedObjectAttributes), [saved_workspace_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts#:~:text=SavedObjectAttributes), [saved_workspace_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts#:~:text=SavedObjectAttributes) | - | @@ -352,6 +393,8 @@ so TS and code-reference navigation might not highlight them. | | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| | | [use_kibana_index_patterns.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_kibana_index_patterns.ts#:~:text=indexPatterns) | - | +| | [use_find_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx#:~:text=SavedObjectAttributes), [use_find_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx#:~:text=SavedObjectAttributes), [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=SavedObjectAttributes), [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=SavedObjectAttributes), [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=SavedObjectAttributes), [use_get_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_get_saved_object.tsx#:~:text=SavedObjectAttributes), [use_get_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_get_saved_object.tsx#:~:text=SavedObjectAttributes), [use_update_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx#:~:text=SavedObjectAttributes), [use_update_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx#:~:text=SavedObjectAttributes), [use_update_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx#:~:text=SavedObjectAttributes)+ 2 more | - | +| | [use_find_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx#:~:text=SavedObjectAttributes), [use_find_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx#:~:text=SavedObjectAttributes), [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=SavedObjectAttributes), [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=SavedObjectAttributes), [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=SavedObjectAttributes), [use_get_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_get_saved_object.tsx#:~:text=SavedObjectAttributes), [use_get_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_get_saved_object.tsx#:~:text=SavedObjectAttributes), [use_update_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx#:~:text=SavedObjectAttributes), [use_update_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx#:~:text=SavedObjectAttributes), [use_update_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx#:~:text=SavedObjectAttributes)+ 2 more | - | @@ -450,10 +493,12 @@ so TS and code-reference navigation might not highlight them. | | | [annotations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/routes/annotations.ts#:~:text=authc) | - | | | [initialization.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/saved_objects/initialization/initialization.ts#:~:text=authz), [sync_task.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/saved_objects/sync_task.ts#:~:text=authz), [plugin.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/plugin.ts#:~:text=authz), [plugin.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/plugin.ts#:~:text=authz) | - | | | [app.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/app.tsx#:~:text=onAppLeave) | 8.8.0 | +| | [modules.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/common/types/modules.ts#:~:text=SavedObjectAttributes), [modules.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/common/types/modules.ts#:~:text=SavedObjectAttributes) | - | | | [errors.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/common/util/errors/errors.test.ts#:~:text=req), [errors.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/common/util/errors/errors.test.ts#:~:text=req) | 8.8.0 Note to maintainers: when looking at usages, mind that typical use could be inside a `catch` block, so TS and code-reference navigation might not highlight them. | +| | [modules.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/common/types/modules.ts#:~:text=SavedObjectAttributes), [modules.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/common/types/modules.ts#:~:text=SavedObjectAttributes) | - | @@ -522,6 +567,8 @@ so TS and code-reference navigation might not highlight them. | | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| | | [saved_object.test.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/saved_object/saved_object.test.ts#:~:text=indexPatterns), [saved_object.test.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/saved_object/saved_object.test.ts#:~:text=indexPatterns) | - | +| | [types.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/types.ts#:~:text=SavedObjectAttributes), [types.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/types.ts#:~:text=SavedObjectAttributes), [types.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/types.ts#:~:text=SavedObjectAttributes), [create_source.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/saved_object/helpers/create_source.ts#:~:text=SavedObjectAttributes), [create_source.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/saved_object/helpers/create_source.ts#:~:text=SavedObjectAttributes), [find_object_by_title.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/saved_object/helpers/find_object_by_title.ts#:~:text=SavedObjectAttributes), [find_object_by_title.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/saved_object/helpers/find_object_by_title.ts#:~:text=SavedObjectAttributes), [save_with_confirmation.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.ts#:~:text=SavedObjectAttributes), [save_with_confirmation.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.ts#:~:text=SavedObjectAttributes), [saved_object.test.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/saved_object/saved_object.test.ts#:~:text=SavedObjectAttributes)+ 13 more | - | +| | [types.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/types.ts#:~:text=SavedObjectAttributes), [types.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/types.ts#:~:text=SavedObjectAttributes), [types.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/types.ts#:~:text=SavedObjectAttributes), [create_source.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/saved_object/helpers/create_source.ts#:~:text=SavedObjectAttributes), [create_source.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/saved_object/helpers/create_source.ts#:~:text=SavedObjectAttributes), [find_object_by_title.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/saved_object/helpers/find_object_by_title.ts#:~:text=SavedObjectAttributes), [find_object_by_title.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/saved_object/helpers/find_object_by_title.ts#:~:text=SavedObjectAttributes), [save_with_confirmation.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.ts#:~:text=SavedObjectAttributes), [save_with_confirmation.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.ts#:~:text=SavedObjectAttributes), [saved_object.test.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_objects/public/saved_object/saved_object.test.ts#:~:text=SavedObjectAttributes)+ 13 more | - | @@ -551,6 +598,15 @@ so TS and code-reference navigation might not highlight them. | +## savedSearch + +| Deprecated API | Reference location(s) | Remove By | +| ---------------|-----------|-----------| +| | [search_migrations.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_search/server/saved_objects/search_migrations.ts#:~:text=SavedObjectAttributes), [search_migrations.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_search/server/saved_objects/search_migrations.ts#:~:text=SavedObjectAttributes) | - | +| | [search_migrations.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_search/server/saved_objects/search_migrations.ts#:~:text=SavedObjectAttributes), [search_migrations.ts](https://github.com/elastic/kibana/tree/main/src/plugins/saved_search/server/saved_objects/search_migrations.ts#:~:text=SavedObjectAttributes) | - | + + + ## searchprofiler | Deprecated API | Reference location(s) | Remove By | @@ -595,11 +651,14 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | | [middleware.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=indexPatterns), [dependencies_start_mock.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts#:~:text=indexPatterns) | - | | | [wrap_search_source_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.ts#:~:text=create) | - | | | [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/wrap_search_source_client.test.ts#:~:text=fetch) | - | +| | [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts#:~:text=options) | - | | | [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [list.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.test.ts#:~:text=mode), [response_actions.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts#:~:text=mode)+ 3 more | 8.8.0 | | | [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [list.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.test.ts#:~:text=mode), [response_actions.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts#:~:text=mode)+ 3 more | 8.8.0 | | | [request_context_factory.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/request_context_factory.ts#:~:text=authc), [request_context_factory.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/request_context_factory.ts#:~:text=authc), [create_signals_migration_route.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.ts#:~:text=authc), [delete_signals_migration_route.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/delete_signals_migration_route.ts#:~:text=authc), [finalize_signals_migration_route.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.ts#:~:text=authc), [open_close_signals_route.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts#:~:text=authc), [preview_rules_route.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts#:~:text=authc), [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts#:~:text=authc) | - | | | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/app/index.tsx#:~:text=onAppLeave), [plugin.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/plugin.tsx#:~:text=onAppLeave) | 8.8.0 | | | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx#:~:text=AppLeaveHandler), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx#:~:text=AppLeaveHandler), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/types.ts#:~:text=AppLeaveHandler), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/types.ts#:~:text=AppLeaveHandler), [routes.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/app/routes.tsx#:~:text=AppLeaveHandler), [routes.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/app/routes.tsx#:~:text=AppLeaveHandler), [app.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/app/app.tsx#:~:text=AppLeaveHandler), [app.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/app/app.tsx#:~:text=AppLeaveHandler), [app.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/app/app.tsx#:~:text=AppLeaveHandler) | 8.8.0 | +| | [legacy_types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_types.ts#:~:text=SavedObjectAttributes), [legacy_types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_types.ts#:~:text=SavedObjectAttributes), [legacy_migrations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.ts#:~:text=SavedObjectAttributes), [legacy_migrations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.ts#:~:text=SavedObjectAttributes) | - | +| | [legacy_types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_types.ts#:~:text=SavedObjectAttributes), [legacy_types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_types.ts#:~:text=SavedObjectAttributes), [legacy_migrations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.ts#:~:text=SavedObjectAttributes), [legacy_migrations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.ts#:~:text=SavedObjectAttributes) | - | @@ -644,6 +703,15 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ +## taskManager + +| Deprecated API | Reference location(s) | Remove By | +| ---------------|-----------|-----------| +| | [task_store.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/task_manager/server/task_store.test.ts#:~:text=SavedObjectAttributes), [task_store.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/task_manager/server/task_store.test.ts#:~:text=SavedObjectAttributes) | - | +| | [task_store.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/task_manager/server/task_store.test.ts#:~:text=SavedObjectAttributes), [task_store.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/task_manager/server/task_store.test.ts#:~:text=SavedObjectAttributes) | - | + + + ## transform | Deprecated API | Reference location(s) | Remove By | @@ -711,6 +779,8 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| | | [visualize_top_nav.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx#:~:text=onAppLeave), [visualize_editor_common.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/public/visualize_app/components/visualize_editor_common.tsx#:~:text=onAppLeave), [app.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/public/visualize_app/app.tsx#:~:text=onAppLeave), [index.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/public/visualize_app/index.tsx#:~:text=onAppLeave) | 8.8.0 | +| | [types.ts](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/common/types.ts#:~:text=SavedObjectAttributes), [types.ts](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/common/types.ts#:~:text=SavedObjectAttributes), [save_with_confirmation.ts](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.ts#:~:text=SavedObjectAttributes), [save_with_confirmation.ts](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.ts#:~:text=SavedObjectAttributes), [find_object_by_title.ts](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/public/utils/saved_objects_utils/find_object_by_title.ts#:~:text=SavedObjectAttributes), [find_object_by_title.ts](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/public/utils/saved_objects_utils/find_object_by_title.ts#:~:text=SavedObjectAttributes), [saved_visualization_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/public/utils/saved_visualization_references/saved_visualization_references.ts#:~:text=SavedObjectAttributes), [saved_visualization_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/public/utils/saved_visualization_references/saved_visualization_references.ts#:~:text=SavedObjectAttributes), [saved_visualize_utils.ts](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/public/utils/saved_visualize_utils.ts#:~:text=SavedObjectAttributes), [saved_visualize_utils.ts](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/public/utils/saved_visualize_utils.ts#:~:text=SavedObjectAttributes)+ 13 more | - | +| | [types.ts](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/common/types.ts#:~:text=SavedObjectAttributes), [types.ts](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/common/types.ts#:~:text=SavedObjectAttributes), [save_with_confirmation.ts](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.ts#:~:text=SavedObjectAttributes), [save_with_confirmation.ts](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.ts#:~:text=SavedObjectAttributes), [find_object_by_title.ts](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/public/utils/saved_objects_utils/find_object_by_title.ts#:~:text=SavedObjectAttributes), [find_object_by_title.ts](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/public/utils/saved_objects_utils/find_object_by_title.ts#:~:text=SavedObjectAttributes), [saved_visualization_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/public/utils/saved_visualization_references/saved_visualization_references.ts#:~:text=SavedObjectAttributes), [saved_visualization_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/public/utils/saved_visualization_references/saved_visualization_references.ts#:~:text=SavedObjectAttributes), [saved_visualize_utils.ts](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/public/utils/saved_visualize_utils.ts#:~:text=SavedObjectAttributes), [saved_visualize_utils.ts](https://github.com/elastic/kibana/tree/main/src/plugins/visualizations/public/utils/saved_visualize_utils.ts#:~:text=SavedObjectAttributes)+ 13 more | - | diff --git a/api_docs/deprecations_by_team.mdx b/api_docs/deprecations_by_team.mdx index 8545316f77c0fe..e47e3f9731bd78 100644 --- a/api_docs/deprecations_by_team.mdx +++ b/api_docs/deprecations_by_team.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsDueByTeam slug: /kibana-dev-docs/api-meta/deprecations-due-by-team title: Deprecated APIs due to be removed, by team description: Lists the teams that are referencing deprecated APIs with a remove by date. -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -80,7 +80,7 @@ so TS and code-reference navigation might not highlight them. | Note to maintainers: when looking at usages, mind that typical use could be inside a `catch` block, so TS and code-reference navigation might not highlight them. | | @kbn/core-saved-objects-migration-server-internal | | [document_migrator.test.ts](https://github.com/elastic/kibana/tree/main/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/core/document_migrator.test.ts#:~:text=warning), [migration_logger.ts](https://github.com/elastic/kibana/tree/main/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/core/migration_logger.ts#:~:text=warning) | 8.8.0 | -| @kbn/core-metrics-server-internal | | [ops_metrics_collector.ts](https://github.com/elastic/kibana/tree/main/packages/core/metrics/core-metrics-server-internal/src/ops_metrics_collector.ts#:~:text=process), [get_ops_metrics_log.ts](https://github.com/elastic/kibana/tree/main/packages/core/metrics/core-metrics-server-internal/src/logging/get_ops_metrics_log.ts#:~:text=process), [get_ops_metrics_log.test.ts](https://github.com/elastic/kibana/tree/main/packages/core/metrics/core-metrics-server-internal/src/logging/get_ops_metrics_log.test.ts#:~:text=process), [core_usage_data_service.ts](https://github.com/elastic/kibana/tree/main/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.ts#:~:text=process), [core_usage_data_service.ts](https://github.com/elastic/kibana/tree/main/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.ts#:~:text=process), [core_usage_data_service.ts](https://github.com/elastic/kibana/tree/main/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.ts#:~:text=process) | 8.8.0 | +| @kbn/core-apps-browser-internal | | [load_status.ts](https://github.com/elastic/kibana/tree/main/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts#:~:text=process), [load_status.ts](https://github.com/elastic/kibana/tree/main/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts#:~:text=process), [load_status.ts](https://github.com/elastic/kibana/tree/main/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts#:~:text=process), [load_status.ts](https://github.com/elastic/kibana/tree/main/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts#:~:text=process), [load_status.ts](https://github.com/elastic/kibana/tree/main/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts#:~:text=process), [load_status.ts](https://github.com/elastic/kibana/tree/main/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts#:~:text=process), [load_status.test.ts](https://github.com/elastic/kibana/tree/main/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts#:~:text=process), [ops_metrics_collector.ts](https://github.com/elastic/kibana/tree/main/packages/core/metrics/core-metrics-server-internal/src/ops_metrics_collector.ts#:~:text=process), [get_ops_metrics_log.ts](https://github.com/elastic/kibana/tree/main/packages/core/metrics/core-metrics-server-internal/src/logging/get_ops_metrics_log.ts#:~:text=process), [get_ops_metrics_log.test.ts](https://github.com/elastic/kibana/tree/main/packages/core/metrics/core-metrics-server-internal/src/logging/get_ops_metrics_log.test.ts#:~:text=process)+ 5 more | 8.8.0 | diff --git a/api_docs/dev_tools.mdx b/api_docs/dev_tools.mdx index 334a2c08e23c39..4d5276b0b5bf79 100644 --- a/api_docs/dev_tools.mdx +++ b/api_docs/dev_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/devTools title: "devTools" image: https://source.unsplash.com/400x175/?github description: API docs for the devTools plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'devTools'] --- import devToolsObj from './dev_tools.devdocs.json'; diff --git a/api_docs/discover.mdx b/api_docs/discover.mdx index 6eb89f79f48077..08de43b84168ee 100644 --- a/api_docs/discover.mdx +++ b/api_docs/discover.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discover title: "discover" image: https://source.unsplash.com/400x175/?github description: API docs for the discover plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discover'] --- import discoverObj from './discover.devdocs.json'; diff --git a/api_docs/discover_enhanced.mdx b/api_docs/discover_enhanced.mdx index 3ddbd42c2bc732..77076d23693400 100644 --- a/api_docs/discover_enhanced.mdx +++ b/api_docs/discover_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discoverEnhanced title: "discoverEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the discoverEnhanced plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discoverEnhanced'] --- import discoverEnhancedObj from './discover_enhanced.devdocs.json'; diff --git a/api_docs/embeddable.devdocs.json b/api_docs/embeddable.devdocs.json index 939fe8507b0954..4f8a65cb51f058 100644 --- a/api_docs/embeddable.devdocs.json +++ b/api_docs/embeddable.devdocs.json @@ -137,9 +137,7 @@ "section": "def-public.EmbeddableFactory", "text": "EmbeddableFactory" }, - " | undefined" + " | undefined" ], "path": "src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts", "deprecated": false, @@ -202,9 +200,7 @@ "section": "def-public.EmbeddableOutput", "text": "EmbeddableOutput" }, - ">, ", - "SavedObjectAttributes", - ">>" + ">, unknown>>" ], "path": "src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts", "deprecated": false, @@ -585,9 +581,7 @@ "section": "def-public.EmbeddableOutput", "text": "EmbeddableOutput" }, - ">, ", - "SavedObjectAttributes", - ">) | undefined" + ">, unknown>) | undefined" ], "path": "src/plugins/embeddable/public/lib/attribute_service/attribute_service.tsx", "deprecated": false, @@ -975,9 +969,7 @@ "section": "def-public.EmbeddableFactory", "text": "EmbeddableFactory" }, - " | undefined" + " | undefined" ], "path": "src/plugins/embeddable/public/lib/containers/container.ts", "deprecated": false, @@ -1782,9 +1774,7 @@ "section": "def-public.EmbeddableFactory", "text": "EmbeddableFactory" }, - ", partial?: Partial) => ", + ", partial?: Partial) => ", { "pluginId": "embeddable", "scope": "common", @@ -1813,9 +1803,7 @@ "section": "def-public.EmbeddableFactory", "text": "EmbeddableFactory" }, - "" + "" ], "path": "src/plugins/embeddable/public/lib/containers/container.ts", "deprecated": false, @@ -2093,9 +2081,7 @@ "section": "def-public.EmbeddableFactory", "text": "EmbeddableFactory" }, - " | undefined" + " | undefined" ], "path": "src/plugins/embeddable/public/lib/actions/edit_panel_action.ts", "deprecated": false, @@ -4675,9 +4661,7 @@ "section": "def-public.IEmbeddable", "text": "IEmbeddable" }, - ", T extends ", - "SavedObjectAttributes", - " = ", + ", T = ", "SavedObjectAttributes", ">(def: ", { @@ -5676,9 +5660,7 @@ "section": "def-public.EmbeddableFactory", "text": "EmbeddableFactory" }, - " | undefined; getAllFactories: () => IterableIterator<", + " | undefined; getAllFactories: () => IterableIterator<", { "pluginId": "embeddable", "scope": "public", @@ -5726,9 +5708,7 @@ "section": "def-public.EmbeddableOutput", "text": "EmbeddableOutput" }, - ">, ", - "SavedObjectAttributes", - ">>; overlays: ", + ">, unknown>>; overlays: ", "OverlayStart", "; notifications: ", "NotificationsStart", @@ -5853,9 +5833,7 @@ "section": "def-public.EmbeddableFactory", "text": "EmbeddableFactory" }, - " | undefined" + " | undefined" ], "path": "src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx", "deprecated": false, @@ -5931,9 +5909,7 @@ "section": "def-public.EmbeddableOutput", "text": "EmbeddableOutput" }, - ">, ", - "SavedObjectAttributes", - ">>" + ">, unknown>>" ], "path": "src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx", "deprecated": false, @@ -10203,9 +10179,7 @@ "section": "def-public.EmbeddableFactoryDefinition", "text": "EmbeddableFactoryDefinition" }, - ") => () => ", + ") => () => ", { "pluginId": "embeddable", "scope": "public", @@ -10213,9 +10187,7 @@ "section": "def-public.EmbeddableFactory", "text": "EmbeddableFactory" }, - "" + "" ], "path": "src/plugins/embeddable/public/plugin.tsx", "deprecated": false, @@ -10251,9 +10223,7 @@ "section": "def-public.EmbeddableFactoryDefinition", "text": "EmbeddableFactoryDefinition" }, - "" + "" ], "path": "src/plugins/embeddable/public/plugin.tsx", "deprecated": false, @@ -10453,9 +10423,7 @@ "section": "def-public.EmbeddableFactory", "text": "EmbeddableFactory" }, - " | undefined" + " | undefined" ], "path": "src/plugins/embeddable/public/plugin.tsx", "deprecated": false, @@ -10535,9 +10503,7 @@ "section": "def-public.EmbeddableOutput", "text": "EmbeddableOutput" }, - ">, ", - "SavedObjectAttributes", - ">>" + ">, unknown>>" ], "path": "src/plugins/embeddable/public/plugin.tsx", "deprecated": false, diff --git a/api_docs/embeddable.mdx b/api_docs/embeddable.mdx index 362609d3d6d49c..46536b7e4e7457 100644 --- a/api_docs/embeddable.mdx +++ b/api_docs/embeddable.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/embeddable title: "embeddable" image: https://source.unsplash.com/400x175/?github description: API docs for the embeddable plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddable'] --- import embeddableObj from './embeddable.devdocs.json'; diff --git a/api_docs/embeddable_enhanced.mdx b/api_docs/embeddable_enhanced.mdx index 58bb7cbddd42b5..d9db07c069266d 100644 --- a/api_docs/embeddable_enhanced.mdx +++ b/api_docs/embeddable_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/embeddableEnhanced title: "embeddableEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the embeddableEnhanced plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddableEnhanced'] --- import embeddableEnhancedObj from './embeddable_enhanced.devdocs.json'; diff --git a/api_docs/encrypted_saved_objects.mdx b/api_docs/encrypted_saved_objects.mdx index 002dd9eb271396..26dade24fcebe8 100644 --- a/api_docs/encrypted_saved_objects.mdx +++ b/api_docs/encrypted_saved_objects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/encryptedSavedObjects title: "encryptedSavedObjects" image: https://source.unsplash.com/400x175/?github description: API docs for the encryptedSavedObjects plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'encryptedSavedObjects'] --- import encryptedSavedObjectsObj from './encrypted_saved_objects.devdocs.json'; diff --git a/api_docs/enterprise_search.mdx b/api_docs/enterprise_search.mdx index 5a45b7486d4c96..d1463f299812a3 100644 --- a/api_docs/enterprise_search.mdx +++ b/api_docs/enterprise_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/enterpriseSearch title: "enterpriseSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the enterpriseSearch plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'enterpriseSearch'] --- import enterpriseSearchObj from './enterprise_search.devdocs.json'; diff --git a/api_docs/es_ui_shared.mdx b/api_docs/es_ui_shared.mdx index b3f3fc87bda00d..33335ea2334ff5 100644 --- a/api_docs/es_ui_shared.mdx +++ b/api_docs/es_ui_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/esUiShared title: "esUiShared" image: https://source.unsplash.com/400x175/?github description: API docs for the esUiShared plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'esUiShared'] --- import esUiSharedObj from './es_ui_shared.devdocs.json'; diff --git a/api_docs/event_annotation.mdx b/api_docs/event_annotation.mdx index 0161dd80da0d72..a81227a1bb1ea9 100644 --- a/api_docs/event_annotation.mdx +++ b/api_docs/event_annotation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventAnnotation title: "eventAnnotation" image: https://source.unsplash.com/400x175/?github description: API docs for the eventAnnotation plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventAnnotation'] --- import eventAnnotationObj from './event_annotation.devdocs.json'; diff --git a/api_docs/event_log.devdocs.json b/api_docs/event_log.devdocs.json index 8e717efd4ad23c..491bb3f1654233 100644 --- a/api_docs/event_log.devdocs.json +++ b/api_docs/event_log.devdocs.json @@ -696,6 +696,48 @@ } ], "returnComment": [] + }, + { + "parentPluginId": "eventLog", + "id": "def-server.ClusterClientAdapter.aggregateEventsWithAuthFilter", + "type": "Function", + "tags": [], + "label": "aggregateEventsWithAuthFilter", + "description": [], + "signature": [ + "(queryOptions: ", + "AggregateEventsWithAuthFilter", + ") => Promise<", + { + "pluginId": "eventLog", + "scope": "server", + "docId": "kibEventLogPluginApi", + "section": "def-server.AggregateEventsBySavedObjectResult", + "text": "AggregateEventsBySavedObjectResult" + }, + ">" + ], + "path": "x-pack/plugins/event_log/server/es/cluster_client_adapter.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "eventLog", + "id": "def-server.ClusterClientAdapter.aggregateEventsWithAuthFilter.$1", + "type": "Object", + "tags": [], + "label": "queryOptions", + "description": [], + "signature": [ + "AggregateEventsWithAuthFilter" + ], + "path": "x-pack/plugins/event_log/server/es/cluster_client_adapter.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] } ], "initialIsOpen": false @@ -1007,6 +1049,82 @@ } ], "returnComment": [] + }, + { + "parentPluginId": "eventLog", + "id": "def-server.IEventLogClient.aggregateEventsWithAuthFilter", + "type": "Function", + "tags": [], + "label": "aggregateEventsWithAuthFilter", + "description": [], + "signature": [ + "(type: string, authFilter: ", + "KueryNode", + ", options?: Partial<", + "AggregateOptionsType", + "> | undefined) => Promise<", + { + "pluginId": "eventLog", + "scope": "server", + "docId": "kibEventLogPluginApi", + "section": "def-server.AggregateEventsBySavedObjectResult", + "text": "AggregateEventsBySavedObjectResult" + }, + ">" + ], + "path": "x-pack/plugins/event_log/server/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "eventLog", + "id": "def-server.IEventLogClient.aggregateEventsWithAuthFilter.$1", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "signature": [ + "string" + ], + "path": "x-pack/plugins/event_log/server/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "eventLog", + "id": "def-server.IEventLogClient.aggregateEventsWithAuthFilter.$2", + "type": "Object", + "tags": [], + "label": "authFilter", + "description": [], + "signature": [ + "KueryNode" + ], + "path": "x-pack/plugins/event_log/server/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "eventLog", + "id": "def-server.IEventLogClient.aggregateEventsWithAuthFilter.$3", + "type": "Object", + "tags": [], + "label": "options", + "description": [], + "signature": [ + "Partial<", + "AggregateOptionsType", + "> | undefined" + ], + "path": "x-pack/plugins/event_log/server/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [] } ], "initialIsOpen": false diff --git a/api_docs/event_log.mdx b/api_docs/event_log.mdx index 3867ef968cdda5..9e511fedb53c35 100644 --- a/api_docs/event_log.mdx +++ b/api_docs/event_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventLog title: "eventLog" image: https://source.unsplash.com/400x175/?github description: API docs for the eventLog plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventLog'] --- import eventLogObj from './event_log.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Response Ops](https://github.com/orgs/elastic/teams/response-ops) for q | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 100 | 0 | 100 | 9 | +| 106 | 0 | 106 | 10 | ## Server diff --git a/api_docs/expression_error.mdx b/api_docs/expression_error.mdx index f8a6757785b98f..779d9fecd0a5a1 100644 --- a/api_docs/expression_error.mdx +++ b/api_docs/expression_error.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionError title: "expressionError" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionError plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionError'] --- import expressionErrorObj from './expression_error.devdocs.json'; diff --git a/api_docs/expression_gauge.mdx b/api_docs/expression_gauge.mdx index 9c151c832439fe..1314c5829ba025 100644 --- a/api_docs/expression_gauge.mdx +++ b/api_docs/expression_gauge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionGauge title: "expressionGauge" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionGauge plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionGauge'] --- import expressionGaugeObj from './expression_gauge.devdocs.json'; diff --git a/api_docs/expression_heatmap.mdx b/api_docs/expression_heatmap.mdx index 18e588eda03225..ad99eefe3cef8e 100644 --- a/api_docs/expression_heatmap.mdx +++ b/api_docs/expression_heatmap.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionHeatmap title: "expressionHeatmap" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionHeatmap plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionHeatmap'] --- import expressionHeatmapObj from './expression_heatmap.devdocs.json'; diff --git a/api_docs/expression_image.mdx b/api_docs/expression_image.mdx index 7c7fcf1e51a85e..373c970e79391e 100644 --- a/api_docs/expression_image.mdx +++ b/api_docs/expression_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionImage title: "expressionImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionImage plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionImage'] --- import expressionImageObj from './expression_image.devdocs.json'; diff --git a/api_docs/expression_legacy_metric_vis.mdx b/api_docs/expression_legacy_metric_vis.mdx index 227caffdd86aef..25595c22d66f34 100644 --- a/api_docs/expression_legacy_metric_vis.mdx +++ b/api_docs/expression_legacy_metric_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionLegacyMetricVis title: "expressionLegacyMetricVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionLegacyMetricVis plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionLegacyMetricVis'] --- import expressionLegacyMetricVisObj from './expression_legacy_metric_vis.devdocs.json'; diff --git a/api_docs/expression_metric.mdx b/api_docs/expression_metric.mdx index 73127734fb39d0..b87a948a300fd1 100644 --- a/api_docs/expression_metric.mdx +++ b/api_docs/expression_metric.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionMetric title: "expressionMetric" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionMetric plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetric'] --- import expressionMetricObj from './expression_metric.devdocs.json'; diff --git a/api_docs/expression_metric_vis.mdx b/api_docs/expression_metric_vis.mdx index ee91044586ad45..169ad67ac1cb51 100644 --- a/api_docs/expression_metric_vis.mdx +++ b/api_docs/expression_metric_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionMetricVis title: "expressionMetricVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionMetricVis plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetricVis'] --- import expressionMetricVisObj from './expression_metric_vis.devdocs.json'; diff --git a/api_docs/expression_partition_vis.mdx b/api_docs/expression_partition_vis.mdx index 30188432c4589b..ba9cd0dd34dff5 100644 --- a/api_docs/expression_partition_vis.mdx +++ b/api_docs/expression_partition_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionPartitionVis title: "expressionPartitionVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionPartitionVis plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionPartitionVis'] --- import expressionPartitionVisObj from './expression_partition_vis.devdocs.json'; diff --git a/api_docs/expression_repeat_image.mdx b/api_docs/expression_repeat_image.mdx index abbd5e0f52ab51..cd945202839331 100644 --- a/api_docs/expression_repeat_image.mdx +++ b/api_docs/expression_repeat_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionRepeatImage title: "expressionRepeatImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionRepeatImage plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRepeatImage'] --- import expressionRepeatImageObj from './expression_repeat_image.devdocs.json'; diff --git a/api_docs/expression_reveal_image.mdx b/api_docs/expression_reveal_image.mdx index ae05b346c5294f..c498eb80733d9a 100644 --- a/api_docs/expression_reveal_image.mdx +++ b/api_docs/expression_reveal_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionRevealImage title: "expressionRevealImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionRevealImage plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRevealImage'] --- import expressionRevealImageObj from './expression_reveal_image.devdocs.json'; diff --git a/api_docs/expression_shape.mdx b/api_docs/expression_shape.mdx index 3df0f71990ee19..e7b8bed1a17862 100644 --- a/api_docs/expression_shape.mdx +++ b/api_docs/expression_shape.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionShape title: "expressionShape" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionShape plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionShape'] --- import expressionShapeObj from './expression_shape.devdocs.json'; diff --git a/api_docs/expression_tagcloud.mdx b/api_docs/expression_tagcloud.mdx index 312da9ec73cca8..57f903f3914cdb 100644 --- a/api_docs/expression_tagcloud.mdx +++ b/api_docs/expression_tagcloud.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionTagcloud title: "expressionTagcloud" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionTagcloud plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionTagcloud'] --- import expressionTagcloudObj from './expression_tagcloud.devdocs.json'; diff --git a/api_docs/expression_x_y.mdx b/api_docs/expression_x_y.mdx index 9720c8b749c920..53e2ea761113db 100644 --- a/api_docs/expression_x_y.mdx +++ b/api_docs/expression_x_y.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionXY title: "expressionXY" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionXY plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionXY'] --- import expressionXYObj from './expression_x_y.devdocs.json'; diff --git a/api_docs/expressions.devdocs.json b/api_docs/expressions.devdocs.json index 21faf370389c9d..fc3d36b421afbc 100644 --- a/api_docs/expressions.devdocs.json +++ b/api_docs/expressions.devdocs.json @@ -38020,7 +38020,9 @@ "section": "def-common.SerializedDatatable", "text": "SerializedDatatable" }, - ") => { rows: _.Dictionary[]; type: \"datatable\"; columns: ", + ") => { rows: ", + "Dictionary", + "[]; type: \"datatable\"; columns: ", { "pluginId": "expressions", "scope": "common", diff --git a/api_docs/expressions.mdx b/api_docs/expressions.mdx index 3bc004877bf6d4..14e2ee54f3dc76 100644 --- a/api_docs/expressions.mdx +++ b/api_docs/expressions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressions title: "expressions" image: https://source.unsplash.com/400x175/?github description: API docs for the expressions plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressions'] --- import expressionsObj from './expressions.devdocs.json'; diff --git a/api_docs/features.mdx b/api_docs/features.mdx index d3bfbec1eafaa5..ed7acfdb8e5a2f 100644 --- a/api_docs/features.mdx +++ b/api_docs/features.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/features title: "features" image: https://source.unsplash.com/400x175/?github description: API docs for the features plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'features'] --- import featuresObj from './features.devdocs.json'; diff --git a/api_docs/field_formats.mdx b/api_docs/field_formats.mdx index 47ac9e94ccb6fe..6d3a4fa2b36cb9 100644 --- a/api_docs/field_formats.mdx +++ b/api_docs/field_formats.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fieldFormats title: "fieldFormats" image: https://source.unsplash.com/400x175/?github description: API docs for the fieldFormats plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fieldFormats'] --- import fieldFormatsObj from './field_formats.devdocs.json'; diff --git a/api_docs/file_upload.mdx b/api_docs/file_upload.mdx index 00ef1834df39bb..28ed55ba062797 100644 --- a/api_docs/file_upload.mdx +++ b/api_docs/file_upload.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fileUpload title: "fileUpload" image: https://source.unsplash.com/400x175/?github description: API docs for the fileUpload plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fileUpload'] --- import fileUploadObj from './file_upload.devdocs.json'; diff --git a/api_docs/files.mdx b/api_docs/files.mdx index b94441baeff075..87497a92b7daf2 100644 --- a/api_docs/files.mdx +++ b/api_docs/files.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/files title: "files" image: https://source.unsplash.com/400x175/?github description: API docs for the files plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'files'] --- import filesObj from './files.devdocs.json'; diff --git a/api_docs/fleet.devdocs.json b/api_docs/fleet.devdocs.json index 267549af758387..de2984a6a41763 100644 --- a/api_docs/fleet.devdocs.json +++ b/api_docs/fleet.devdocs.json @@ -979,6 +979,37 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "fleet", + "id": "def-public.PackageGenericErrorsListProps", + "type": "Interface", + "tags": [], + "label": "PackageGenericErrorsListProps", + "description": [], + "path": "x-pack/plugins/fleet/public/types/ui_extensions.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "fleet", + "id": "def-public.PackageGenericErrorsListProps.packageErrors", + "type": "Array", + "tags": [], + "label": "packageErrors", + "description": [ + "A list of errors from a package" + ], + "signature": [ + "FleetServerAgentComponentUnit", + "[]" + ], + "path": "x-pack/plugins/fleet/public/types/ui_extensions.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "fleet", "id": "def-public.PackagePolicyCreateExtension", @@ -1894,10 +1925,10 @@ "id": "def-public.UIExtensionsStorage.Unnamed", "type": "IndexSignature", "tags": [], - "label": "[key: string]: Partial>", + "label": "[key: string]: Partial>", "description": [], "signature": [ - "[key: string]: Partial | React.FunctionComponent<", + { + "pluginId": "fleet", + "scope": "public", + "docId": "kibFleetPluginApi", + "section": "def-public.PackageGenericErrorsListProps", + "text": "PackageGenericErrorsListProps" + }, + ">" + ], + "path": "x-pack/plugins/fleet/public/types/ui_extensions.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "fleet", "id": "def-public.PackagePolicyCreateExtensionComponent", @@ -2197,6 +2261,8 @@ "text": "PackageAssetsExtension" }, " | ", + "PackageGenericErrorsListExtension", + " | ", { "pluginId": "fleet", "scope": "public", @@ -2291,6 +2357,8 @@ "text": "PackageAssetsExtension" }, " | ", + "PackageGenericErrorsListExtension", + " | ", { "pluginId": "fleet", "scope": "public", @@ -3555,6 +3623,8 @@ "text": "PackageAssetsExtension" }, " | ", + "PackageGenericErrorsListExtension", + " | ", { "pluginId": "fleet", "scope": "public", @@ -4784,6 +4854,26 @@ "path": "x-pack/plugins/fleet/server/plugin.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "fleet", + "id": "def-server.FleetSetupDeps.taskManager", + "type": "Object", + "tags": [], + "label": "taskManager", + "description": [], + "signature": [ + { + "pluginId": "taskManager", + "scope": "server", + "docId": "kibTaskManagerPluginApi", + "section": "def-server.TaskManagerSetupContract", + "text": "TaskManagerSetupContract" + } + ], + "path": "x-pack/plugins/fleet/server/plugin.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -5372,14 +5462,8 @@ ", esClient: ", "ElasticsearchClient", ", packagePolicies: ", - { - "pluginId": "fleet", - "scope": "common", - "docId": "kibFleetPluginApi", - "section": "def-common.NewPackagePolicy", - "text": "NewPackagePolicy" - }, - "[], agentPolicyId: string, options?: { user?: ", + "NewPackagePolicyWithId", + "[], options?: { user?: ", { "pluginId": "security", "scope": "common", @@ -5387,7 +5471,7 @@ "section": "def-common.AuthenticatedUser", "text": "AuthenticatedUser" }, - " | undefined; bumpRevision?: boolean | undefined; } | undefined) => Promise<", + " | undefined; bumpRevision?: boolean | undefined; force?: true | undefined; } | undefined) => Promise<", { "pluginId": "fleet", "scope": "common", @@ -5439,13 +5523,7 @@ "label": "packagePolicies", "description": [], "signature": [ - { - "pluginId": "fleet", - "scope": "common", - "docId": "kibFleetPluginApi", - "section": "def-common.NewPackagePolicy", - "text": "NewPackagePolicy" - }, + "NewPackagePolicyWithId", "[]" ], "path": "x-pack/plugins/fleet/server/services/package_policy.ts", @@ -5456,21 +5534,6 @@ { "parentPluginId": "fleet", "id": "def-server.PackagePolicyServiceInterface.bulkCreate.$4", - "type": "string", - "tags": [], - "label": "agentPolicyId", - "description": [], - "signature": [ - "string" - ], - "path": "x-pack/plugins/fleet/server/services/package_policy.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - }, - { - "parentPluginId": "fleet", - "id": "def-server.PackagePolicyServiceInterface.bulkCreate.$5", "type": "Object", "tags": [], "label": "options", @@ -5481,7 +5544,7 @@ "children": [ { "parentPluginId": "fleet", - "id": "def-server.PackagePolicyServiceInterface.bulkCreate.$5.user", + "id": "def-server.PackagePolicyServiceInterface.bulkCreate.$4.user", "type": "Object", "tags": [], "label": "user", @@ -5502,7 +5565,7 @@ }, { "parentPluginId": "fleet", - "id": "def-server.PackagePolicyServiceInterface.bulkCreate.$5.bumpRevision", + "id": "def-server.PackagePolicyServiceInterface.bulkCreate.$4.bumpRevision", "type": "CompoundType", "tags": [], "label": "bumpRevision", @@ -5513,6 +5576,20 @@ "path": "x-pack/plugins/fleet/server/services/package_policy.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "fleet", + "id": "def-server.PackagePolicyServiceInterface.bulkCreate.$4.force", + "type": "boolean", + "tags": [], + "label": "force", + "description": [], + "signature": [ + "true | undefined" + ], + "path": "x-pack/plugins/fleet/server/services/package_policy.ts", + "deprecated": false, + "trackAdoption": false } ] } @@ -6823,7 +6900,9 @@ "section": "def-common.PackageInfo", "text": "PackageInfo" }, - "; }>" + "; experimentalDataStreamFeatures: ", + "ExperimentalDataStreamFeature", + "[]; }>" ], "path": "x-pack/plugins/fleet/server/services/package_policy.ts", "deprecated": false, @@ -10495,6 +10574,20 @@ "path": "x-pack/plugins/fleet/common/types/models/epm.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "fleet", + "id": "def-common.Installation.experimental_data_stream_features", + "type": "Array", + "tags": [], + "label": "experimental_data_stream_features", + "description": [], + "signature": [ + "{ data_stream: string; features: Record<\"synthetic_source\", boolean>; }[] | undefined" + ], + "path": "x-pack/plugins/fleet/common/types/models/epm.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -11501,6 +11594,21 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "fleet", + "id": "def-common.NewPackagePolicyInputStream.release", + "type": "CompoundType", + "tags": [], + "label": "release", + "description": [], + "signature": [ + "RegistryRelease", + " | undefined" + ], + "path": "x-pack/plugins/fleet/common/types/models/package_policy.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "fleet", "id": "def-common.NewPackagePolicyInputStream.vars", @@ -11901,6 +12009,21 @@ "path": "x-pack/plugins/fleet/common/types/models/package_policy.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "fleet", + "id": "def-common.PackagePolicyPackage.experimental_data_stream_features", + "type": "Array", + "tags": [], + "label": "experimental_data_stream_features", + "description": [], + "signature": [ + "ExperimentalDataStreamFeature", + "[] | undefined" + ], + "path": "x-pack/plugins/fleet/common/types/models/package_policy.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -12430,10 +12553,13 @@ { "parentPluginId": "fleet", "id": "def-common.RegistryDataStream.RegistryDataStreamKeys.release", - "type": "string", + "type": "CompoundType", "tags": [], "label": "[RegistryDataStreamKeys.release]", "description": [], + "signature": [ + "\"experimental\" | \"beta\" | \"ga\"" + ], "path": "x-pack/plugins/fleet/common/types/models/epm.ts", "deprecated": false, "trackAdoption": false @@ -15255,6 +15381,17 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "fleet", + "id": "def-common.AGENT_API_ROUTES.ACTION_STATUS_PATTERN", + "type": "string", + "tags": [], + "label": "ACTION_STATUS_PATTERN", + "description": [], + "path": "x-pack/plugins/fleet/common/constants/routes.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "fleet", "id": "def-common.AGENT_API_ROUTES.LIST_TAGS_PATTERN", @@ -15950,6 +16087,22 @@ "children": [], "returnComment": [] }, + { + "parentPluginId": "fleet", + "id": "def-common.agentRouteService.getActionStatusPath", + "type": "Function", + "tags": [], + "label": "getActionStatusPath", + "description": [], + "signature": [ + "() => string" + ], + "path": "x-pack/plugins/fleet/common/services/routes.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, { "parentPluginId": "fleet", "id": "def-common.agentRouteService.getCurrentUpgradesPath", diff --git a/api_docs/fleet.mdx b/api_docs/fleet.mdx index 212a823589def5..c34e123cc7f1d8 100644 --- a/api_docs/fleet.mdx +++ b/api_docs/fleet.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fleet title: "fleet" image: https://source.unsplash.com/400x175/?github description: API docs for the fleet plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fleet'] --- import fleetObj from './fleet.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Fleet](https://github.com/orgs/elastic/teams/fleet) for questions regar | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 970 | 3 | 873 | 10 | +| 979 | 3 | 880 | 15 | ## Client diff --git a/api_docs/global_search.mdx b/api_docs/global_search.mdx index 151db33c2bbd07..c0573b0ecb7b70 100644 --- a/api_docs/global_search.mdx +++ b/api_docs/global_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/globalSearch title: "globalSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the globalSearch plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'globalSearch'] --- import globalSearchObj from './global_search.devdocs.json'; diff --git a/api_docs/home.mdx b/api_docs/home.mdx index 0633e4a8248504..bcfa3dfec7958d 100644 --- a/api_docs/home.mdx +++ b/api_docs/home.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/home title: "home" image: https://source.unsplash.com/400x175/?github description: API docs for the home plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'home'] --- import homeObj from './home.devdocs.json'; diff --git a/api_docs/index_lifecycle_management.mdx b/api_docs/index_lifecycle_management.mdx index 7cd6a257eeca1a..f4708381681255 100644 --- a/api_docs/index_lifecycle_management.mdx +++ b/api_docs/index_lifecycle_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/indexLifecycleManagement title: "indexLifecycleManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the indexLifecycleManagement plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexLifecycleManagement'] --- import indexLifecycleManagementObj from './index_lifecycle_management.devdocs.json'; diff --git a/api_docs/index_management.mdx b/api_docs/index_management.mdx index b83bcf59070e4a..7d85bd1ec01f71 100644 --- a/api_docs/index_management.mdx +++ b/api_docs/index_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/indexManagement title: "indexManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the indexManagement plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexManagement'] --- import indexManagementObj from './index_management.devdocs.json'; diff --git a/api_docs/infra.mdx b/api_docs/infra.mdx index 05616b0cfadc4e..dce762d8e8ee09 100644 --- a/api_docs/infra.mdx +++ b/api_docs/infra.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/infra title: "infra" image: https://source.unsplash.com/400x175/?github description: API docs for the infra plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'infra'] --- import infraObj from './infra.devdocs.json'; diff --git a/api_docs/inspector.mdx b/api_docs/inspector.mdx index 8454800519cb03..31c4af63a555c5 100644 --- a/api_docs/inspector.mdx +++ b/api_docs/inspector.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/inspector title: "inspector" image: https://source.unsplash.com/400x175/?github description: API docs for the inspector plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'inspector'] --- import inspectorObj from './inspector.devdocs.json'; diff --git a/api_docs/interactive_setup.mdx b/api_docs/interactive_setup.mdx index 100b024c58ddf9..ed977814da5d11 100644 --- a/api_docs/interactive_setup.mdx +++ b/api_docs/interactive_setup.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/interactiveSetup title: "interactiveSetup" image: https://source.unsplash.com/400x175/?github description: API docs for the interactiveSetup plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'interactiveSetup'] --- import interactiveSetupObj from './interactive_setup.devdocs.json'; diff --git a/api_docs/kbn_ace.mdx b/api_docs/kbn_ace.mdx index 4606d800cf22e9..6621697f1c9f63 100644 --- a/api_docs/kbn_ace.mdx +++ b/api_docs/kbn_ace.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ace title: "@kbn/ace" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ace plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ace'] --- import kbnAceObj from './kbn_ace.devdocs.json'; diff --git a/api_docs/kbn_aiops_components.mdx b/api_docs/kbn_aiops_components.mdx index 765ff1f91971fd..bf305d28029512 100644 --- a/api_docs/kbn_aiops_components.mdx +++ b/api_docs/kbn_aiops_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-components title: "@kbn/aiops-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-components plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-components'] --- import kbnAiopsComponentsObj from './kbn_aiops_components.devdocs.json'; diff --git a/api_docs/kbn_aiops_utils.devdocs.json b/api_docs/kbn_aiops_utils.devdocs.json index 3a2e48949a52bc..e2ece6ca9c92cd 100644 --- a/api_docs/kbn_aiops_utils.devdocs.json +++ b/api_docs/kbn_aiops_utils.devdocs.json @@ -186,7 +186,9 @@ "\nOverload to set up a string based response stream with support\nfor gzip compression depending on provided request headers.\n" ], "signature": [ - "(headers: Headers, logger: ", + "(headers: ", + "Headers", + ", logger: ", "Logger", ") => StreamFactoryReturnType" ], @@ -197,7 +199,7 @@ { "parentPluginId": "@kbn/aiops-utils", "id": "def-common.streamFactory.$1", - "type": "Object", + "type": "CompoundType", "tags": [], "label": "headers", "description": [ @@ -242,7 +244,9 @@ "\nSets up a response stream with support for gzip compression depending on provided\nrequest headers. Any non-string data pushed to the stream will be stream as NDJSON.\n" ], "signature": [ - "(headers: Headers, logger: ", + "(headers: ", + "Headers", + ", logger: ", "Logger", ") => StreamFactoryReturnType" ], @@ -253,7 +257,7 @@ { "parentPluginId": "@kbn/aiops-utils", "id": "def-common.streamFactory.$1", - "type": "Object", + "type": "CompoundType", "tags": [], "label": "headers", "description": [ diff --git a/api_docs/kbn_aiops_utils.mdx b/api_docs/kbn_aiops_utils.mdx index 4b51bc25be8909..03363d0929e0ec 100644 --- a/api_docs/kbn_aiops_utils.mdx +++ b/api_docs/kbn_aiops_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-utils title: "@kbn/aiops-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-utils plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-utils'] --- import kbnAiopsUtilsObj from './kbn_aiops_utils.devdocs.json'; diff --git a/api_docs/kbn_alerts.mdx b/api_docs/kbn_alerts.mdx index d5a04ff5eee17e..334c27aa6c8a29 100644 --- a/api_docs/kbn_alerts.mdx +++ b/api_docs/kbn_alerts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerts title: "@kbn/alerts" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerts plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts'] --- import kbnAlertsObj from './kbn_alerts.devdocs.json'; diff --git a/api_docs/kbn_analytics.mdx b/api_docs/kbn_analytics.mdx index a99efc95e8c132..9383407fefd72a 100644 --- a/api_docs/kbn_analytics.mdx +++ b/api_docs/kbn_analytics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics title: "@kbn/analytics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics'] --- import kbnAnalyticsObj from './kbn_analytics.devdocs.json'; diff --git a/api_docs/kbn_analytics_client.mdx b/api_docs/kbn_analytics_client.mdx index 1fd92e71b327ac..e98185590f8c3f 100644 --- a/api_docs/kbn_analytics_client.mdx +++ b/api_docs/kbn_analytics_client.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-client title: "@kbn/analytics-client" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-client plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-client'] --- import kbnAnalyticsClientObj from './kbn_analytics_client.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx index 4f9fac4a0b2ece..be15a3999a2e2f 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-browser title: "@kbn/analytics-shippers-elastic-v3-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-browser plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-browser'] --- import kbnAnalyticsShippersElasticV3BrowserObj from './kbn_analytics_shippers_elastic_v3_browser.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx index ada937d1aa30eb..8afe51725ee3de 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-common title: "@kbn/analytics-shippers-elastic-v3-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-common plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-common'] --- import kbnAnalyticsShippersElasticV3CommonObj from './kbn_analytics_shippers_elastic_v3_common.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx index 360208b56403ba..e51dfc55facb5f 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-server title: "@kbn/analytics-shippers-elastic-v3-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-server plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-server'] --- import kbnAnalyticsShippersElasticV3ServerObj from './kbn_analytics_shippers_elastic_v3_server.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_fullstory.mdx b/api_docs/kbn_analytics_shippers_fullstory.mdx index 995d23de7fcc71..dd9ef3297f3fc9 100644 --- a/api_docs/kbn_analytics_shippers_fullstory.mdx +++ b/api_docs/kbn_analytics_shippers_fullstory.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-fullstory title: "@kbn/analytics-shippers-fullstory" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-fullstory plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-fullstory'] --- import kbnAnalyticsShippersFullstoryObj from './kbn_analytics_shippers_fullstory.devdocs.json'; diff --git a/api_docs/kbn_apm_config_loader.mdx b/api_docs/kbn_apm_config_loader.mdx index 41246db0a67567..3ae58b4c1af9a3 100644 --- a/api_docs/kbn_apm_config_loader.mdx +++ b/api_docs/kbn_apm_config_loader.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-config-loader title: "@kbn/apm-config-loader" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-config-loader plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-config-loader'] --- import kbnApmConfigLoaderObj from './kbn_apm_config_loader.devdocs.json'; diff --git a/api_docs/kbn_apm_synthtrace.devdocs.json b/api_docs/kbn_apm_synthtrace.devdocs.json index 647bcc12376234..3ac0d3855ecb17 100644 --- a/api_docs/kbn_apm_synthtrace.devdocs.json +++ b/api_docs/kbn_apm_synthtrace.devdocs.json @@ -1052,7 +1052,7 @@ "label": "service", "description": [], "signature": [ - "(name: string, environment: string, agentName: string) => ", + "({ name, environment, agentName, }: { name: string; environment: string; agentName: string; }) => ", "Service" ], "path": "packages/kbn-apm-synthtrace/src/lib/apm/index.ts", @@ -1063,32 +1063,13 @@ { "parentPluginId": "@kbn/apm-synthtrace", "id": "def-server.apm.service.$1", - "type": "string", - "tags": [], - "label": "name", - "description": [], - "path": "packages/kbn-apm-synthtrace/src/lib/apm/service.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "@kbn/apm-synthtrace", - "id": "def-server.apm.service.$2", - "type": "string", - "tags": [], - "label": "environment", - "description": [], - "path": "packages/kbn-apm-synthtrace/src/lib/apm/service.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "@kbn/apm-synthtrace", - "id": "def-server.apm.service.$3", - "type": "string", + "type": "Object", "tags": [], - "label": "agentName", + "label": "__0", "description": [], + "signature": [ + "{ name: string; environment: string; agentName: string; }" + ], "path": "packages/kbn-apm-synthtrace/src/lib/apm/service.ts", "deprecated": false, "trackAdoption": false @@ -1103,7 +1084,7 @@ "label": "browser", "description": [], "signature": [ - "(serviceName: string, production: string, userAgent: Partial<{ 'user_agent.original': string; 'user_agent.os.name': string; 'user_agent.name': string; 'user_agent.device.name': string; 'user_agent.version': number; }>) => ", + "({ serviceName, environment, userAgent, }: { serviceName: string; environment: string; userAgent: Partial<{ 'user_agent.original': string; 'user_agent.os.name': string; 'user_agent.name': string; 'user_agent.device.name': string; 'user_agent.version': number; }>; }) => ", "Browser" ], "path": "packages/kbn-apm-synthtrace/src/lib/apm/index.ts", @@ -1114,34 +1095,12 @@ { "parentPluginId": "@kbn/apm-synthtrace", "id": "def-server.apm.browser.$1", - "type": "string", - "tags": [], - "label": "serviceName", - "description": [], - "path": "packages/kbn-apm-synthtrace/src/lib/apm/browser.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "@kbn/apm-synthtrace", - "id": "def-server.apm.browser.$2", - "type": "string", - "tags": [], - "label": "production", - "description": [], - "path": "packages/kbn-apm-synthtrace/src/lib/apm/browser.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "@kbn/apm-synthtrace", - "id": "def-server.apm.browser.$3", "type": "Object", "tags": [], - "label": "userAgent", + "label": "__0", "description": [], "signature": [ - "{ 'user_agent.original'?: string | undefined; 'user_agent.os.name'?: string | undefined; 'user_agent.name'?: string | undefined; 'user_agent.device.name'?: string | undefined; 'user_agent.version'?: number | undefined; }" + "{ serviceName: string; environment: string; userAgent: Partial<{ 'user_agent.original': string; 'user_agent.os.name': string; 'user_agent.name': string; 'user_agent.device.name': string; 'user_agent.version': number; }>; }" ], "path": "packages/kbn-apm-synthtrace/src/lib/apm/browser.ts", "deprecated": false, diff --git a/api_docs/kbn_apm_synthtrace.mdx b/api_docs/kbn_apm_synthtrace.mdx index 82c57310848d5d..504c9bca4c0fa0 100644 --- a/api_docs/kbn_apm_synthtrace.mdx +++ b/api_docs/kbn_apm_synthtrace.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-synthtrace title: "@kbn/apm-synthtrace" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-synthtrace plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-synthtrace'] --- import kbnApmSynthtraceObj from './kbn_apm_synthtrace.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 76 | 0 | 76 | 12 | +| 72 | 0 | 72 | 12 | ## Server diff --git a/api_docs/kbn_apm_utils.mdx b/api_docs/kbn_apm_utils.mdx index 3b8859bf24577a..275f7b012bfd94 100644 --- a/api_docs/kbn_apm_utils.mdx +++ b/api_docs/kbn_apm_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-utils title: "@kbn/apm-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-utils plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-utils'] --- import kbnApmUtilsObj from './kbn_apm_utils.devdocs.json'; diff --git a/api_docs/kbn_axe_config.mdx b/api_docs/kbn_axe_config.mdx index a84beaaf76f9c7..cf1ad1d13b377a 100644 --- a/api_docs/kbn_axe_config.mdx +++ b/api_docs/kbn_axe_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-axe-config title: "@kbn/axe-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/axe-config plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/axe-config'] --- import kbnAxeConfigObj from './kbn_axe_config.devdocs.json'; diff --git a/api_docs/kbn_chart_icons.mdx b/api_docs/kbn_chart_icons.mdx index f81ea7eb893f4c..c7046482625d78 100644 --- a/api_docs/kbn_chart_icons.mdx +++ b/api_docs/kbn_chart_icons.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-chart-icons title: "@kbn/chart-icons" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/chart-icons plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/chart-icons'] --- import kbnChartIconsObj from './kbn_chart_icons.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_core.mdx b/api_docs/kbn_ci_stats_core.mdx index f136b1b4b9722e..23ed91d2c66be5 100644 --- a/api_docs/kbn_ci_stats_core.mdx +++ b/api_docs/kbn_ci_stats_core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-core title: "@kbn/ci-stats-core" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-core plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-core'] --- import kbnCiStatsCoreObj from './kbn_ci_stats_core.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_performance_metrics.mdx b/api_docs/kbn_ci_stats_performance_metrics.mdx index 4fac55f5f28f2f..5257456208105e 100644 --- a/api_docs/kbn_ci_stats_performance_metrics.mdx +++ b/api_docs/kbn_ci_stats_performance_metrics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-performance-metrics title: "@kbn/ci-stats-performance-metrics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-performance-metrics plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-performance-metrics'] --- import kbnCiStatsPerformanceMetricsObj from './kbn_ci_stats_performance_metrics.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_reporter.mdx b/api_docs/kbn_ci_stats_reporter.mdx index 9a986e49442eb8..b15d655b56a536 100644 --- a/api_docs/kbn_ci_stats_reporter.mdx +++ b/api_docs/kbn_ci_stats_reporter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-reporter title: "@kbn/ci-stats-reporter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-reporter plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-reporter'] --- import kbnCiStatsReporterObj from './kbn_ci_stats_reporter.devdocs.json'; diff --git a/api_docs/kbn_cli_dev_mode.mdx b/api_docs/kbn_cli_dev_mode.mdx index ad805a672ec6af..f932ba7c041f17 100644 --- a/api_docs/kbn_cli_dev_mode.mdx +++ b/api_docs/kbn_cli_dev_mode.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cli-dev-mode title: "@kbn/cli-dev-mode" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cli-dev-mode plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cli-dev-mode'] --- import kbnCliDevModeObj from './kbn_cli_dev_mode.devdocs.json'; diff --git a/api_docs/kbn_coloring.mdx b/api_docs/kbn_coloring.mdx index d48d41a5c86497..8bb8e4dc836a11 100644 --- a/api_docs/kbn_coloring.mdx +++ b/api_docs/kbn_coloring.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-coloring title: "@kbn/coloring" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/coloring plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/coloring'] --- import kbnColoringObj from './kbn_coloring.devdocs.json'; diff --git a/api_docs/kbn_config.mdx b/api_docs/kbn_config.mdx index 1e6fbb0c526198..5018d8bb4eada9 100644 --- a/api_docs/kbn_config.mdx +++ b/api_docs/kbn_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config title: "@kbn/config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config'] --- import kbnConfigObj from './kbn_config.devdocs.json'; diff --git a/api_docs/kbn_config_mocks.mdx b/api_docs/kbn_config_mocks.mdx index 4ed5acbd873d26..ab5303e61f5035 100644 --- a/api_docs/kbn_config_mocks.mdx +++ b/api_docs/kbn_config_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config-mocks title: "@kbn/config-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-mocks'] --- import kbnConfigMocksObj from './kbn_config_mocks.devdocs.json'; diff --git a/api_docs/kbn_config_schema.mdx b/api_docs/kbn_config_schema.mdx index 1a85935c87425d..900b6d1e3c2600 100644 --- a/api_docs/kbn_config_schema.mdx +++ b/api_docs/kbn_config_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config-schema title: "@kbn/config-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config-schema plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-schema'] --- import kbnConfigSchemaObj from './kbn_config_schema.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser.mdx b/api_docs/kbn_core_analytics_browser.mdx index ac39a7961593a8..dc587f2160061c 100644 --- a/api_docs/kbn_core_analytics_browser.mdx +++ b/api_docs/kbn_core_analytics_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser title: "@kbn/core-analytics-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser'] --- import kbnCoreAnalyticsBrowserObj from './kbn_core_analytics_browser.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser_internal.mdx b/api_docs/kbn_core_analytics_browser_internal.mdx index fc5cd3bfb84380..0b284d23158cd5 100644 --- a/api_docs/kbn_core_analytics_browser_internal.mdx +++ b/api_docs/kbn_core_analytics_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-internal title: "@kbn/core-analytics-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-internal'] --- import kbnCoreAnalyticsBrowserInternalObj from './kbn_core_analytics_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser_mocks.mdx b/api_docs/kbn_core_analytics_browser_mocks.mdx index 4c9c75e8523909..58495bfef63630 100644 --- a/api_docs/kbn_core_analytics_browser_mocks.mdx +++ b/api_docs/kbn_core_analytics_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-mocks title: "@kbn/core-analytics-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-mocks'] --- import kbnCoreAnalyticsBrowserMocksObj from './kbn_core_analytics_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server.mdx b/api_docs/kbn_core_analytics_server.mdx index 92c14242978161..bd436d6c38c713 100644 --- a/api_docs/kbn_core_analytics_server.mdx +++ b/api_docs/kbn_core_analytics_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server title: "@kbn/core-analytics-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server'] --- import kbnCoreAnalyticsServerObj from './kbn_core_analytics_server.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server_internal.mdx b/api_docs/kbn_core_analytics_server_internal.mdx index da3c85dc76a4e6..a184b698d620cb 100644 --- a/api_docs/kbn_core_analytics_server_internal.mdx +++ b/api_docs/kbn_core_analytics_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server-internal title: "@kbn/core-analytics-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-internal'] --- import kbnCoreAnalyticsServerInternalObj from './kbn_core_analytics_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server_mocks.mdx b/api_docs/kbn_core_analytics_server_mocks.mdx index e291353f701f69..a22a699e12e6bd 100644 --- a/api_docs/kbn_core_analytics_server_mocks.mdx +++ b/api_docs/kbn_core_analytics_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server-mocks title: "@kbn/core-analytics-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-mocks'] --- import kbnCoreAnalyticsServerMocksObj from './kbn_core_analytics_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser.mdx b/api_docs/kbn_core_application_browser.mdx index e1d48305d2290f..5235b62213653a 100644 --- a/api_docs/kbn_core_application_browser.mdx +++ b/api_docs/kbn_core_application_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser title: "@kbn/core-application-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser'] --- import kbnCoreApplicationBrowserObj from './kbn_core_application_browser.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser_internal.mdx b/api_docs/kbn_core_application_browser_internal.mdx index 1b208d9a634874..531cbebfb35ad7 100644 --- a/api_docs/kbn_core_application_browser_internal.mdx +++ b/api_docs/kbn_core_application_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser-internal title: "@kbn/core-application-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser-internal'] --- import kbnCoreApplicationBrowserInternalObj from './kbn_core_application_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser_mocks.mdx b/api_docs/kbn_core_application_browser_mocks.mdx index 3ad4f503057fac..c4b574b0450daf 100644 --- a/api_docs/kbn_core_application_browser_mocks.mdx +++ b/api_docs/kbn_core_application_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser-mocks title: "@kbn/core-application-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser-mocks'] --- import kbnCoreApplicationBrowserMocksObj from './kbn_core_application_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_application_common.mdx b/api_docs/kbn_core_application_common.mdx index 4779f2f4f68b53..85eef07ed34990 100644 --- a/api_docs/kbn_core_application_common.mdx +++ b/api_docs/kbn_core_application_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-common title: "@kbn/core-application-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-common plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-common'] --- import kbnCoreApplicationCommonObj from './kbn_core_application_common.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_internal.devdocs.json b/api_docs/kbn_core_apps_browser_internal.devdocs.json new file mode 100644 index 00000000000000..5cea64788f8991 --- /dev/null +++ b/api_docs/kbn_core_apps_browser_internal.devdocs.json @@ -0,0 +1,355 @@ +{ + "id": "@kbn/core-apps-browser-internal", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [ + { + "parentPluginId": "@kbn/core-apps-browser-internal", + "id": "def-common.CoreAppsService", + "type": "Class", + "tags": [], + "label": "CoreAppsService", + "description": [], + "path": "packages/core/apps/core-apps-browser-internal/src/core_app.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-apps-browser-internal", + "id": "def-common.CoreAppsService.Unnamed", + "type": "Function", + "tags": [], + "label": "Constructor", + "description": [], + "signature": [ + "any" + ], + "path": "packages/core/apps/core-apps-browser-internal/src/core_app.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-apps-browser-internal", + "id": "def-common.CoreAppsService.Unnamed.$1", + "type": "Object", + "tags": [], + "label": "coreContext", + "description": [], + "signature": [ + "CoreContext" + ], + "path": "packages/core/apps/core-apps-browser-internal/src/core_app.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/core-apps-browser-internal", + "id": "def-common.CoreAppsService.setup", + "type": "Function", + "tags": [], + "label": "setup", + "description": [], + "signature": [ + "({ application, http, injectedMetadata, notifications }: ", + { + "pluginId": "@kbn/core-apps-browser-internal", + "scope": "common", + "docId": "kibKbnCoreAppsBrowserInternalPluginApi", + "section": "def-common.CoreAppsServiceSetupDeps", + "text": "CoreAppsServiceSetupDeps" + }, + ") => void" + ], + "path": "packages/core/apps/core-apps-browser-internal/src/core_app.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-apps-browser-internal", + "id": "def-common.CoreAppsService.setup.$1", + "type": "Object", + "tags": [], + "label": "{ application, http, injectedMetadata, notifications }", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-apps-browser-internal", + "scope": "common", + "docId": "kibKbnCoreAppsBrowserInternalPluginApi", + "section": "def-common.CoreAppsServiceSetupDeps", + "text": "CoreAppsServiceSetupDeps" + } + ], + "path": "packages/core/apps/core-apps-browser-internal/src/core_app.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/core-apps-browser-internal", + "id": "def-common.CoreAppsService.start", + "type": "Function", + "tags": [], + "label": "start", + "description": [], + "signature": [ + "({ application, docLinks, http, notifications, uiSettings, }: ", + { + "pluginId": "@kbn/core-apps-browser-internal", + "scope": "common", + "docId": "kibKbnCoreAppsBrowserInternalPluginApi", + "section": "def-common.CoreAppsServiceStartDeps", + "text": "CoreAppsServiceStartDeps" + }, + ") => void" + ], + "path": "packages/core/apps/core-apps-browser-internal/src/core_app.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-apps-browser-internal", + "id": "def-common.CoreAppsService.start.$1", + "type": "Object", + "tags": [], + "label": "{\n application,\n docLinks,\n http,\n notifications,\n uiSettings,\n }", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-apps-browser-internal", + "scope": "common", + "docId": "kibKbnCoreAppsBrowserInternalPluginApi", + "section": "def-common.CoreAppsServiceStartDeps", + "text": "CoreAppsServiceStartDeps" + } + ], + "path": "packages/core/apps/core-apps-browser-internal/src/core_app.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/core-apps-browser-internal", + "id": "def-common.CoreAppsService.stop", + "type": "Function", + "tags": [], + "label": "stop", + "description": [], + "signature": [ + "() => void" + ], + "path": "packages/core/apps/core-apps-browser-internal/src/core_app.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + } + ], + "initialIsOpen": false + } + ], + "functions": [], + "interfaces": [ + { + "parentPluginId": "@kbn/core-apps-browser-internal", + "id": "def-common.CoreAppsServiceSetupDeps", + "type": "Interface", + "tags": [], + "label": "CoreAppsServiceSetupDeps", + "description": [], + "path": "packages/core/apps/core-apps-browser-internal/src/core_app.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-apps-browser-internal", + "id": "def-common.CoreAppsServiceSetupDeps.application", + "type": "Object", + "tags": [], + "label": "application", + "description": [], + "signature": [ + "InternalApplicationSetup" + ], + "path": "packages/core/apps/core-apps-browser-internal/src/core_app.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-apps-browser-internal", + "id": "def-common.CoreAppsServiceSetupDeps.http", + "type": "Object", + "tags": [], + "label": "http", + "description": [], + "signature": [ + "HttpSetup" + ], + "path": "packages/core/apps/core-apps-browser-internal/src/core_app.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-apps-browser-internal", + "id": "def-common.CoreAppsServiceSetupDeps.injectedMetadata", + "type": "Object", + "tags": [], + "label": "injectedMetadata", + "description": [], + "signature": [ + "InternalInjectedMetadataSetup" + ], + "path": "packages/core/apps/core-apps-browser-internal/src/core_app.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-apps-browser-internal", + "id": "def-common.CoreAppsServiceSetupDeps.notifications", + "type": "Object", + "tags": [], + "label": "notifications", + "description": [], + "signature": [ + "NotificationsSetup" + ], + "path": "packages/core/apps/core-apps-browser-internal/src/core_app.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-apps-browser-internal", + "id": "def-common.CoreAppsServiceStartDeps", + "type": "Interface", + "tags": [], + "label": "CoreAppsServiceStartDeps", + "description": [], + "path": "packages/core/apps/core-apps-browser-internal/src/core_app.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-apps-browser-internal", + "id": "def-common.CoreAppsServiceStartDeps.application", + "type": "Object", + "tags": [], + "label": "application", + "description": [], + "signature": [ + "InternalApplicationStart" + ], + "path": "packages/core/apps/core-apps-browser-internal/src/core_app.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-apps-browser-internal", + "id": "def-common.CoreAppsServiceStartDeps.docLinks", + "type": "Object", + "tags": [], + "label": "docLinks", + "description": [], + "signature": [ + "DocLinksStart" + ], + "path": "packages/core/apps/core-apps-browser-internal/src/core_app.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-apps-browser-internal", + "id": "def-common.CoreAppsServiceStartDeps.http", + "type": "Object", + "tags": [], + "label": "http", + "description": [], + "signature": [ + "HttpSetup" + ], + "path": "packages/core/apps/core-apps-browser-internal/src/core_app.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-apps-browser-internal", + "id": "def-common.CoreAppsServiceStartDeps.notifications", + "type": "Object", + "tags": [], + "label": "notifications", + "description": [], + "signature": [ + "NotificationsStart" + ], + "path": "packages/core/apps/core-apps-browser-internal/src/core_app.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-apps-browser-internal", + "id": "def-common.CoreAppsServiceStartDeps.uiSettings", + "type": "Object", + "tags": [], + "label": "uiSettings", + "description": [], + "signature": [ + "IUiSettingsClient" + ], + "path": "packages/core/apps/core-apps-browser-internal/src/core_app.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], + "enums": [], + "misc": [ + { + "parentPluginId": "@kbn/core-apps-browser-internal", + "id": "def-common.URL_MAX_LENGTH", + "type": "CompoundType", + "tags": [], + "label": "URL_MAX_LENGTH", + "description": [ + "\nThe max URL length allowed by the current browser. Should be used to display warnings to users when query parameters\ncause URL to exceed this limit." + ], + "signature": [ + "2000 | 25000" + ], + "path": "packages/core/apps/core-apps-browser-internal/src/errors/url_overflow.tsx", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_core_apps_browser_internal.mdx b/api_docs/kbn_core_apps_browser_internal.mdx new file mode 100644 index 00000000000000..c7286945082028 --- /dev/null +++ b/api_docs/kbn_core_apps_browser_internal.mdx @@ -0,0 +1,36 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnCoreAppsBrowserInternalPluginApi +slug: /kibana-dev-docs/api/kbn-core-apps-browser-internal +title: "@kbn/core-apps-browser-internal" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/core-apps-browser-internal plugin +date: 2022-09-14 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-internal'] +--- +import kbnCoreAppsBrowserInternalObj from './kbn_core_apps_browser_internal.devdocs.json'; + + + +Contact Kibana Core for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 20 | 0 | 19 | 0 | + +## Common + +### Classes + + +### Interfaces + + +### Consts, variables and types + + diff --git a/api_docs/kbn_core_apps_browser_mocks.devdocs.json b/api_docs/kbn_core_apps_browser_mocks.devdocs.json new file mode 100644 index 00000000000000..7e58c029fc73be --- /dev/null +++ b/api_docs/kbn_core_apps_browser_mocks.devdocs.json @@ -0,0 +1,58 @@ +{ + "id": "@kbn/core-apps-browser-mocks", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [ + { + "parentPluginId": "@kbn/core-apps-browser-mocks", + "id": "def-common.coreAppsMock", + "type": "Object", + "tags": [], + "label": "coreAppsMock", + "description": [], + "path": "packages/core/apps/core-apps-browser-mocks/src/core_app.mock.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-apps-browser-mocks", + "id": "def-common.coreAppsMock.create", + "type": "Function", + "tags": [], + "label": "create", + "description": [], + "signature": [ + "() => jest.Mocked" + ], + "path": "packages/core/apps/core-apps-browser-mocks/src/core_app.mock.ts", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [] + } + ], + "initialIsOpen": false + } + ] + } +} \ No newline at end of file diff --git a/api_docs/kbn_core_apps_browser_mocks.mdx b/api_docs/kbn_core_apps_browser_mocks.mdx new file mode 100644 index 00000000000000..13770a33aabe89 --- /dev/null +++ b/api_docs/kbn_core_apps_browser_mocks.mdx @@ -0,0 +1,30 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnCoreAppsBrowserMocksPluginApi +slug: /kibana-dev-docs/api/kbn-core-apps-browser-mocks +title: "@kbn/core-apps-browser-mocks" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/core-apps-browser-mocks plugin +date: 2022-09-14 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-mocks'] +--- +import kbnCoreAppsBrowserMocksObj from './kbn_core_apps_browser_mocks.devdocs.json'; + + + +Contact Kibana Core for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 2 | 0 | 2 | 0 | + +## Common + +### Objects + + diff --git a/api_docs/kbn_core_base_browser_mocks.mdx b/api_docs/kbn_core_base_browser_mocks.mdx index 701892c07d6fb6..f9406ab2fea114 100644 --- a/api_docs/kbn_core_base_browser_mocks.mdx +++ b/api_docs/kbn_core_base_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-browser-mocks title: "@kbn/core-base-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-browser-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-browser-mocks'] --- import kbnCoreBaseBrowserMocksObj from './kbn_core_base_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_base_common.devdocs.json b/api_docs/kbn_core_base_common.devdocs.json index 5a164327bf3384..e5fae7dc02d767 100644 --- a/api_docs/kbn_core_base_common.devdocs.json +++ b/api_docs/kbn_core_base_common.devdocs.json @@ -142,109 +142,6 @@ } ], "initialIsOpen": false - }, - { - "parentPluginId": "@kbn/core-base-common", - "id": "def-server.ServiceStatus", - "type": "Interface", - "tags": [], - "label": "ServiceStatus", - "description": [ - "\nThe current status of a service at a point in time.\n" - ], - "signature": [ - { - "pluginId": "@kbn/core-base-common", - "scope": "server", - "docId": "kibKbnCoreBaseCommonPluginApi", - "section": "def-server.ServiceStatus", - "text": "ServiceStatus" - }, - "" - ], - "path": "packages/core/base/core-base-common/src/service_status.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "@kbn/core-base-common", - "id": "def-server.ServiceStatus.level", - "type": "CompoundType", - "tags": [], - "label": "level", - "description": [ - "\nThe current availability level of the service." - ], - "signature": [ - "Readonly<{ toString: () => \"available\"; valueOf: () => 0; toJSON: () => \"available\"; }> | Readonly<{ toString: () => \"degraded\"; valueOf: () => 1; toJSON: () => \"degraded\"; }> | Readonly<{ toString: () => \"unavailable\"; valueOf: () => 2; toJSON: () => \"unavailable\"; }> | Readonly<{ toString: () => \"critical\"; valueOf: () => 3; toJSON: () => \"critical\"; }>" - ], - "path": "packages/core/base/core-base-common/src/service_status.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "@kbn/core-base-common", - "id": "def-server.ServiceStatus.summary", - "type": "string", - "tags": [], - "label": "summary", - "description": [ - "\nA high-level summary of the service status." - ], - "path": "packages/core/base/core-base-common/src/service_status.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "@kbn/core-base-common", - "id": "def-server.ServiceStatus.detail", - "type": "string", - "tags": [], - "label": "detail", - "description": [ - "\nA more detailed description of the service status." - ], - "signature": [ - "string | undefined" - ], - "path": "packages/core/base/core-base-common/src/service_status.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "@kbn/core-base-common", - "id": "def-server.ServiceStatus.documentationUrl", - "type": "string", - "tags": [], - "label": "documentationUrl", - "description": [ - "\nA URL to open in a new tab about how to resolve or troubleshoot the problem." - ], - "signature": [ - "string | undefined" - ], - "path": "packages/core/base/core-base-common/src/service_status.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "@kbn/core-base-common", - "id": "def-server.ServiceStatus.meta", - "type": "Uncategorized", - "tags": [], - "label": "meta", - "description": [ - "\nAny JSON-serializable data to be included in the HTTP API response. Useful for providing more fine-grained,\nmachine-readable information about the service status. May include status information for underlying features." - ], - "signature": [ - "Meta | undefined" - ], - "path": "packages/core/base/core-base-common/src/service_status.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false } ], "enums": [ @@ -308,44 +205,9 @@ "deprecated": false, "trackAdoption": false, "initialIsOpen": false - }, - { - "parentPluginId": "@kbn/core-base-common", - "id": "def-server.ServiceStatusLevel", - "type": "Type", - "tags": [], - "label": "ServiceStatusLevel", - "description": [ - "\nA convenience type that represents the union of each value in {@link ServiceStatusLevels}." - ], - "signature": [ - "Readonly<{ toString: () => \"available\"; valueOf: () => 0; toJSON: () => \"available\"; }> | Readonly<{ toString: () => \"degraded\"; valueOf: () => 1; toJSON: () => \"degraded\"; }> | Readonly<{ toString: () => \"unavailable\"; valueOf: () => 2; toJSON: () => \"unavailable\"; }> | Readonly<{ toString: () => \"critical\"; valueOf: () => 3; toJSON: () => \"critical\"; }>" - ], - "path": "packages/core/base/core-base-common/src/service_status.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false } ], - "objects": [ - { - "parentPluginId": "@kbn/core-base-common", - "id": "def-server.ServiceStatusLevels", - "type": "Object", - "tags": [], - "label": "ServiceStatusLevels", - "description": [ - "\nThe current \"level\" of availability of a service.\n" - ], - "signature": [ - "{ readonly available: Readonly<{ toString: () => \"available\"; valueOf: () => 0; toJSON: () => \"available\"; }>; readonly degraded: Readonly<{ toString: () => \"degraded\"; valueOf: () => 1; toJSON: () => \"degraded\"; }>; readonly unavailable: Readonly<{ toString: () => \"unavailable\"; valueOf: () => 2; toJSON: () => \"unavailable\"; }>; readonly critical: Readonly<{ toString: () => \"critical\"; valueOf: () => 3; toJSON: () => \"critical\"; }>; }" - ], - "path": "packages/core/base/core-base-common/src/service_status.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - } - ] + "objects": [] }, "common": { "classes": [], diff --git a/api_docs/kbn_core_base_common.mdx b/api_docs/kbn_core_base_common.mdx index 6f65974f3df957..ca50e0de22338e 100644 --- a/api_docs/kbn_core_base_common.mdx +++ b/api_docs/kbn_core_base_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-common title: "@kbn/core-base-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-common plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-common'] --- import kbnCoreBaseCommonObj from './kbn_core_base_common.devdocs.json'; @@ -21,13 +21,10 @@ Contact Kibana Core for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 20 | 0 | 3 | 0 | +| 12 | 0 | 3 | 0 | ## Server -### Objects - - ### Interfaces diff --git a/api_docs/kbn_core_base_server_internal.mdx b/api_docs/kbn_core_base_server_internal.mdx index 3ac31dbf66d235..7bdaa5116db81b 100644 --- a/api_docs/kbn_core_base_server_internal.mdx +++ b/api_docs/kbn_core_base_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-internal title: "@kbn/core-base-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-internal'] --- import kbnCoreBaseServerInternalObj from './kbn_core_base_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_base_server_mocks.mdx b/api_docs/kbn_core_base_server_mocks.mdx index 5d9da0822c070a..fd66501cdc4d9e 100644 --- a/api_docs/kbn_core_base_server_mocks.mdx +++ b/api_docs/kbn_core_base_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-mocks title: "@kbn/core-base-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-mocks'] --- import kbnCoreBaseServerMocksObj from './kbn_core_base_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_browser_mocks.mdx b/api_docs/kbn_core_capabilities_browser_mocks.mdx index 0fb84b17ee969d..b5f28087bc11c1 100644 --- a/api_docs/kbn_core_capabilities_browser_mocks.mdx +++ b/api_docs/kbn_core_capabilities_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-browser-mocks title: "@kbn/core-capabilities-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-browser-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-browser-mocks'] --- import kbnCoreCapabilitiesBrowserMocksObj from './kbn_core_capabilities_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_common.mdx b/api_docs/kbn_core_capabilities_common.mdx index 24dc0372c51ec7..0c9a5cf8b50821 100644 --- a/api_docs/kbn_core_capabilities_common.mdx +++ b/api_docs/kbn_core_capabilities_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-common title: "@kbn/core-capabilities-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-common plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-common'] --- import kbnCoreCapabilitiesCommonObj from './kbn_core_capabilities_common.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_server.mdx b/api_docs/kbn_core_capabilities_server.mdx index a52d64e5e6e200..b2337825a380f3 100644 --- a/api_docs/kbn_core_capabilities_server.mdx +++ b/api_docs/kbn_core_capabilities_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-server title: "@kbn/core-capabilities-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-server plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-server'] --- import kbnCoreCapabilitiesServerObj from './kbn_core_capabilities_server.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_server_mocks.mdx b/api_docs/kbn_core_capabilities_server_mocks.mdx index 5e2bfe3318f47c..84acbc21937d55 100644 --- a/api_docs/kbn_core_capabilities_server_mocks.mdx +++ b/api_docs/kbn_core_capabilities_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-server-mocks title: "@kbn/core-capabilities-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-server-mocks'] --- import kbnCoreCapabilitiesServerMocksObj from './kbn_core_capabilities_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_chrome_browser.mdx b/api_docs/kbn_core_chrome_browser.mdx index 8e3c4efe990f0d..7ef24aecb41112 100644 --- a/api_docs/kbn_core_chrome_browser.mdx +++ b/api_docs/kbn_core_chrome_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-chrome-browser title: "@kbn/core-chrome-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-chrome-browser plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-chrome-browser'] --- import kbnCoreChromeBrowserObj from './kbn_core_chrome_browser.devdocs.json'; diff --git a/api_docs/kbn_core_chrome_browser_mocks.mdx b/api_docs/kbn_core_chrome_browser_mocks.mdx index ecc9bc1bad7b3b..ae79c6dc60f9e6 100644 --- a/api_docs/kbn_core_chrome_browser_mocks.mdx +++ b/api_docs/kbn_core_chrome_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-chrome-browser-mocks title: "@kbn/core-chrome-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-chrome-browser-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-chrome-browser-mocks'] --- import kbnCoreChromeBrowserMocksObj from './kbn_core_chrome_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_config_server_internal.mdx b/api_docs/kbn_core_config_server_internal.mdx index fa73a6310dd904..5c81c7ec2e974a 100644 --- a/api_docs/kbn_core_config_server_internal.mdx +++ b/api_docs/kbn_core_config_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-config-server-internal title: "@kbn/core-config-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-config-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-config-server-internal'] --- import kbnCoreConfigServerInternalObj from './kbn_core_config_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser.mdx b/api_docs/kbn_core_deprecations_browser.mdx index e9d543f5eab88e..3d91ca307b2b41 100644 --- a/api_docs/kbn_core_deprecations_browser.mdx +++ b/api_docs/kbn_core_deprecations_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser title: "@kbn/core-deprecations-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser'] --- import kbnCoreDeprecationsBrowserObj from './kbn_core_deprecations_browser.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser_internal.mdx b/api_docs/kbn_core_deprecations_browser_internal.mdx index 5e380c589df455..cb965c5aa5a1af 100644 --- a/api_docs/kbn_core_deprecations_browser_internal.mdx +++ b/api_docs/kbn_core_deprecations_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser-internal title: "@kbn/core-deprecations-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser-internal'] --- import kbnCoreDeprecationsBrowserInternalObj from './kbn_core_deprecations_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser_mocks.mdx b/api_docs/kbn_core_deprecations_browser_mocks.mdx index 86d55e1e9c44d1..eb18bbf41caf80 100644 --- a/api_docs/kbn_core_deprecations_browser_mocks.mdx +++ b/api_docs/kbn_core_deprecations_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser-mocks title: "@kbn/core-deprecations-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser-mocks'] --- import kbnCoreDeprecationsBrowserMocksObj from './kbn_core_deprecations_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_common.mdx b/api_docs/kbn_core_deprecations_common.mdx index 6dd116c3c46f26..1b0aebb3766064 100644 --- a/api_docs/kbn_core_deprecations_common.mdx +++ b/api_docs/kbn_core_deprecations_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-common title: "@kbn/core-deprecations-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-common plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-common'] --- import kbnCoreDeprecationsCommonObj from './kbn_core_deprecations_common.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server.mdx b/api_docs/kbn_core_deprecations_server.mdx index 96418b9c64b682..7bb293c4cf2650 100644 --- a/api_docs/kbn_core_deprecations_server.mdx +++ b/api_docs/kbn_core_deprecations_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server title: "@kbn/core-deprecations-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server'] --- import kbnCoreDeprecationsServerObj from './kbn_core_deprecations_server.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server_internal.mdx b/api_docs/kbn_core_deprecations_server_internal.mdx index b557d4e5c473ab..62069be73fa136 100644 --- a/api_docs/kbn_core_deprecations_server_internal.mdx +++ b/api_docs/kbn_core_deprecations_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server-internal title: "@kbn/core-deprecations-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server-internal'] --- import kbnCoreDeprecationsServerInternalObj from './kbn_core_deprecations_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server_mocks.mdx b/api_docs/kbn_core_deprecations_server_mocks.mdx index 0cacd094cfbb99..65814bef740118 100644 --- a/api_docs/kbn_core_deprecations_server_mocks.mdx +++ b/api_docs/kbn_core_deprecations_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server-mocks title: "@kbn/core-deprecations-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server-mocks'] --- import kbnCoreDeprecationsServerMocksObj from './kbn_core_deprecations_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_browser.mdx b/api_docs/kbn_core_doc_links_browser.mdx index 2c72b3d00c5990..f40438ef55a00c 100644 --- a/api_docs/kbn_core_doc_links_browser.mdx +++ b/api_docs/kbn_core_doc_links_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser title: "@kbn/core-doc-links-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-browser plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser'] --- import kbnCoreDocLinksBrowserObj from './kbn_core_doc_links_browser.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_browser_mocks.mdx b/api_docs/kbn_core_doc_links_browser_mocks.mdx index 1582e2b3c6a478..10eb34d7abf42b 100644 --- a/api_docs/kbn_core_doc_links_browser_mocks.mdx +++ b/api_docs/kbn_core_doc_links_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser-mocks title: "@kbn/core-doc-links-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-browser-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser-mocks'] --- import kbnCoreDocLinksBrowserMocksObj from './kbn_core_doc_links_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_server.mdx b/api_docs/kbn_core_doc_links_server.mdx index 7cad22cf1928bb..5ecd9a97cd0451 100644 --- a/api_docs/kbn_core_doc_links_server.mdx +++ b/api_docs/kbn_core_doc_links_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server title: "@kbn/core-doc-links-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-server plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server'] --- import kbnCoreDocLinksServerObj from './kbn_core_doc_links_server.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_server_mocks.mdx b/api_docs/kbn_core_doc_links_server_mocks.mdx index ac6c785eb5bbc8..1d327cc86b8e61 100644 --- a/api_docs/kbn_core_doc_links_server_mocks.mdx +++ b/api_docs/kbn_core_doc_links_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server-mocks title: "@kbn/core-doc-links-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server-mocks'] --- import kbnCoreDocLinksServerMocksObj from './kbn_core_doc_links_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_client_server_internal.mdx b/api_docs/kbn_core_elasticsearch_client_server_internal.mdx index 65546108902bde..99d90f775590e9 100644 --- a/api_docs/kbn_core_elasticsearch_client_server_internal.mdx +++ b/api_docs/kbn_core_elasticsearch_client_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-client-server-internal title: "@kbn/core-elasticsearch-client-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-client-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-client-server-internal'] --- import kbnCoreElasticsearchClientServerInternalObj from './kbn_core_elasticsearch_client_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx b/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx index b119bfb2ad808a..e1069ce57741fb 100644 --- a/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx +++ b/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-client-server-mocks title: "@kbn/core-elasticsearch-client-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-client-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-client-server-mocks'] --- import kbnCoreElasticsearchClientServerMocksObj from './kbn_core_elasticsearch_client_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server.mdx b/api_docs/kbn_core_elasticsearch_server.mdx index d8d8afa63313df..92451f9620ae7a 100644 --- a/api_docs/kbn_core_elasticsearch_server.mdx +++ b/api_docs/kbn_core_elasticsearch_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server title: "@kbn/core-elasticsearch-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server'] --- import kbnCoreElasticsearchServerObj from './kbn_core_elasticsearch_server.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server_internal.devdocs.json b/api_docs/kbn_core_elasticsearch_server_internal.devdocs.json index 9ddb76f9557d64..a040b2f6fb20c2 100644 --- a/api_docs/kbn_core_elasticsearch_server_internal.devdocs.json +++ b/api_docs/kbn_core_elasticsearch_server_internal.devdocs.json @@ -2884,7 +2884,7 @@ "label": "ElasticsearchConfigType", "description": [], "signature": [ - "{ readonly username?: string | undefined; readonly password?: string | undefined; readonly serviceAccountToken?: string | undefined; readonly ssl: Readonly<{ key?: string | undefined; certificateAuthorities?: string | string[] | undefined; certificate?: string | undefined; keyPassphrase?: string | undefined; } & { verificationMode: \"none\" | \"full\" | \"certificate\"; keystore: Readonly<{ path?: string | undefined; password?: string | undefined; } & {}>; truststore: Readonly<{ path?: string | undefined; password?: string | undefined; } & {}>; alwaysPresentCertificate: boolean; }>; readonly healthCheck: Readonly<{} & { delay: moment.Duration; }>; readonly customHeaders: Record; readonly hosts: string | string[]; readonly sniffOnStart: boolean; readonly sniffInterval: false | moment.Duration; readonly sniffOnConnectionFault: boolean; readonly maxSockets: number; readonly compression: boolean; readonly requestHeadersWhitelist: string | string[]; readonly shardTimeout: moment.Duration; readonly requestTimeout: moment.Duration; readonly pingTimeout: moment.Duration; readonly logQueries: boolean; readonly apiVersion: string; readonly ignoreVersionMismatch: boolean; readonly skipStartupConnectionCheck: boolean; }" + "{ readonly username?: string | undefined; readonly password?: string | undefined; readonly serviceAccountToken?: string | undefined; readonly requestTimeout: moment.Duration; readonly compression: boolean; readonly ssl: Readonly<{ key?: string | undefined; certificateAuthorities?: string | string[] | undefined; certificate?: string | undefined; keyPassphrase?: string | undefined; } & { verificationMode: \"none\" | \"full\" | \"certificate\"; keystore: Readonly<{ path?: string | undefined; password?: string | undefined; } & {}>; truststore: Readonly<{ path?: string | undefined; password?: string | undefined; } & {}>; alwaysPresentCertificate: boolean; }>; readonly healthCheck: Readonly<{} & { delay: moment.Duration; }>; readonly customHeaders: Record; readonly hosts: string | string[]; readonly sniffOnStart: boolean; readonly sniffInterval: false | moment.Duration; readonly sniffOnConnectionFault: boolean; readonly maxSockets: number; readonly requestHeadersWhitelist: string | string[]; readonly shardTimeout: moment.Duration; readonly pingTimeout: moment.Duration; readonly logQueries: boolean; readonly apiVersion: string; readonly ignoreVersionMismatch: boolean; readonly skipStartupConnectionCheck: boolean; }" ], "path": "packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.ts", "deprecated": false, diff --git a/api_docs/kbn_core_elasticsearch_server_internal.mdx b/api_docs/kbn_core_elasticsearch_server_internal.mdx index 7bb38a3c5f0bf3..3926f6f01a8369 100644 --- a/api_docs/kbn_core_elasticsearch_server_internal.mdx +++ b/api_docs/kbn_core_elasticsearch_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server-internal title: "@kbn/core-elasticsearch-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server-internal'] --- import kbnCoreElasticsearchServerInternalObj from './kbn_core_elasticsearch_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server_mocks.mdx b/api_docs/kbn_core_elasticsearch_server_mocks.mdx index da30359337d2fc..a9cb24bec12318 100644 --- a/api_docs/kbn_core_elasticsearch_server_mocks.mdx +++ b/api_docs/kbn_core_elasticsearch_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server-mocks title: "@kbn/core-elasticsearch-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server-mocks'] --- import kbnCoreElasticsearchServerMocksObj from './kbn_core_elasticsearch_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_environment_server_internal.mdx b/api_docs/kbn_core_environment_server_internal.mdx index 115eabde42da7c..6af66f8ba0055c 100644 --- a/api_docs/kbn_core_environment_server_internal.mdx +++ b/api_docs/kbn_core_environment_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-environment-server-internal title: "@kbn/core-environment-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-environment-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-environment-server-internal'] --- import kbnCoreEnvironmentServerInternalObj from './kbn_core_environment_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_environment_server_mocks.mdx b/api_docs/kbn_core_environment_server_mocks.mdx index edfcddc56edc18..17ea9c2e1fa3a1 100644 --- a/api_docs/kbn_core_environment_server_mocks.mdx +++ b/api_docs/kbn_core_environment_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-environment-server-mocks title: "@kbn/core-environment-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-environment-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-environment-server-mocks'] --- import kbnCoreEnvironmentServerMocksObj from './kbn_core_environment_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser.mdx b/api_docs/kbn_core_execution_context_browser.mdx index 6d3b3e2cf0095e..c1f928bb433758 100644 --- a/api_docs/kbn_core_execution_context_browser.mdx +++ b/api_docs/kbn_core_execution_context_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser title: "@kbn/core-execution-context-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser'] --- import kbnCoreExecutionContextBrowserObj from './kbn_core_execution_context_browser.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser_internal.mdx b/api_docs/kbn_core_execution_context_browser_internal.mdx index fcc2284968ba8b..01e9be74bb7f5b 100644 --- a/api_docs/kbn_core_execution_context_browser_internal.mdx +++ b/api_docs/kbn_core_execution_context_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser-internal title: "@kbn/core-execution-context-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser-internal'] --- import kbnCoreExecutionContextBrowserInternalObj from './kbn_core_execution_context_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser_mocks.mdx b/api_docs/kbn_core_execution_context_browser_mocks.mdx index 48919a14adc5da..e08bd89edec118 100644 --- a/api_docs/kbn_core_execution_context_browser_mocks.mdx +++ b/api_docs/kbn_core_execution_context_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser-mocks title: "@kbn/core-execution-context-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser-mocks'] --- import kbnCoreExecutionContextBrowserMocksObj from './kbn_core_execution_context_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_common.mdx b/api_docs/kbn_core_execution_context_common.mdx index 0a87811f9fa6d1..c2ddb64eae79ff 100644 --- a/api_docs/kbn_core_execution_context_common.mdx +++ b/api_docs/kbn_core_execution_context_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-common title: "@kbn/core-execution-context-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-common plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-common'] --- import kbnCoreExecutionContextCommonObj from './kbn_core_execution_context_common.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server.mdx b/api_docs/kbn_core_execution_context_server.mdx index be640b0bd6c12f..3761d21583a494 100644 --- a/api_docs/kbn_core_execution_context_server.mdx +++ b/api_docs/kbn_core_execution_context_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server title: "@kbn/core-execution-context-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server'] --- import kbnCoreExecutionContextServerObj from './kbn_core_execution_context_server.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server_internal.mdx b/api_docs/kbn_core_execution_context_server_internal.mdx index 4d9363ee6d0670..c0eb4247d58ec2 100644 --- a/api_docs/kbn_core_execution_context_server_internal.mdx +++ b/api_docs/kbn_core_execution_context_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server-internal title: "@kbn/core-execution-context-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server-internal'] --- import kbnCoreExecutionContextServerInternalObj from './kbn_core_execution_context_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server_mocks.mdx b/api_docs/kbn_core_execution_context_server_mocks.mdx index 8e1632be18c1bc..f2fc5b80d3d1f1 100644 --- a/api_docs/kbn_core_execution_context_server_mocks.mdx +++ b/api_docs/kbn_core_execution_context_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server-mocks title: "@kbn/core-execution-context-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server-mocks'] --- import kbnCoreExecutionContextServerMocksObj from './kbn_core_execution_context_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_fatal_errors_browser.mdx b/api_docs/kbn_core_fatal_errors_browser.mdx index 7fee5984f817aa..18c81b666a64e1 100644 --- a/api_docs/kbn_core_fatal_errors_browser.mdx +++ b/api_docs/kbn_core_fatal_errors_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-fatal-errors-browser title: "@kbn/core-fatal-errors-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-fatal-errors-browser plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-fatal-errors-browser'] --- import kbnCoreFatalErrorsBrowserObj from './kbn_core_fatal_errors_browser.devdocs.json'; diff --git a/api_docs/kbn_core_fatal_errors_browser_mocks.mdx b/api_docs/kbn_core_fatal_errors_browser_mocks.mdx index c989c3c38eff42..16c520b8d62674 100644 --- a/api_docs/kbn_core_fatal_errors_browser_mocks.mdx +++ b/api_docs/kbn_core_fatal_errors_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-fatal-errors-browser-mocks title: "@kbn/core-fatal-errors-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-fatal-errors-browser-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-fatal-errors-browser-mocks'] --- import kbnCoreFatalErrorsBrowserMocksObj from './kbn_core_fatal_errors_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser.mdx b/api_docs/kbn_core_http_browser.mdx index ab44b913d6d22f..caa1ff06437175 100644 --- a/api_docs/kbn_core_http_browser.mdx +++ b/api_docs/kbn_core_http_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser title: "@kbn/core-http-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser'] --- import kbnCoreHttpBrowserObj from './kbn_core_http_browser.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser_internal.mdx b/api_docs/kbn_core_http_browser_internal.mdx index a23bfab2355724..5151c4e5efad10 100644 --- a/api_docs/kbn_core_http_browser_internal.mdx +++ b/api_docs/kbn_core_http_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser-internal title: "@kbn/core-http-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser-internal'] --- import kbnCoreHttpBrowserInternalObj from './kbn_core_http_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser_mocks.mdx b/api_docs/kbn_core_http_browser_mocks.mdx index cc4ba299fcd2f8..95dab9567bc984 100644 --- a/api_docs/kbn_core_http_browser_mocks.mdx +++ b/api_docs/kbn_core_http_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser-mocks title: "@kbn/core-http-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser-mocks'] --- import kbnCoreHttpBrowserMocksObj from './kbn_core_http_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_common.mdx b/api_docs/kbn_core_http_common.mdx index f3ad4235ca0bdd..b2ba2cbbfd7971 100644 --- a/api_docs/kbn_core_http_common.mdx +++ b/api_docs/kbn_core_http_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-common title: "@kbn/core-http-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-common plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-common'] --- import kbnCoreHttpCommonObj from './kbn_core_http_common.devdocs.json'; diff --git a/api_docs/kbn_core_http_context_server_mocks.mdx b/api_docs/kbn_core_http_context_server_mocks.mdx index 8e036459534723..68d92c262a0bf7 100644 --- a/api_docs/kbn_core_http_context_server_mocks.mdx +++ b/api_docs/kbn_core_http_context_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-context-server-mocks title: "@kbn/core-http-context-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-context-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-context-server-mocks'] --- import kbnCoreHttpContextServerMocksObj from './kbn_core_http_context_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_router_server_internal.mdx b/api_docs/kbn_core_http_router_server_internal.mdx index cd7e2beebf898a..00cdb53d4cbf8a 100644 --- a/api_docs/kbn_core_http_router_server_internal.mdx +++ b/api_docs/kbn_core_http_router_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-router-server-internal title: "@kbn/core-http-router-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-router-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-router-server-internal'] --- import kbnCoreHttpRouterServerInternalObj from './kbn_core_http_router_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_router_server_mocks.mdx b/api_docs/kbn_core_http_router_server_mocks.mdx index 355ed41119b58b..9ae005377384f0 100644 --- a/api_docs/kbn_core_http_router_server_mocks.mdx +++ b/api_docs/kbn_core_http_router_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-router-server-mocks title: "@kbn/core-http-router-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-router-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-router-server-mocks'] --- import kbnCoreHttpRouterServerMocksObj from './kbn_core_http_router_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_server.mdx b/api_docs/kbn_core_http_server.mdx index bca3d59fc66ba0..3d72f9991e55d5 100644 --- a/api_docs/kbn_core_http_server.mdx +++ b/api_docs/kbn_core_http_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server title: "@kbn/core-http-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server'] --- import kbnCoreHttpServerObj from './kbn_core_http_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_server_internal.devdocs.json b/api_docs/kbn_core_http_server_internal.devdocs.json index 03cad7bd46025e..236a7a2f4f797e 100644 --- a/api_docs/kbn_core_http_server_internal.devdocs.json +++ b/api_docs/kbn_core_http_server_internal.devdocs.json @@ -759,7 +759,7 @@ "label": "HttpConfigType", "description": [], "signature": [ - "{ readonly basePath?: string | undefined; readonly uuid?: string | undefined; readonly publicBaseUrl?: string | undefined; readonly host: string; readonly name: string; readonly ssl: Readonly<{ key?: string | undefined; certificateAuthorities?: string | string[] | undefined; certificate?: string | undefined; keyPassphrase?: string | undefined; redirectHttpFromPort?: number | undefined; } & { enabled: boolean; keystore: Readonly<{ path?: string | undefined; password?: string | undefined; } & {}>; truststore: Readonly<{ path?: string | undefined; password?: string | undefined; } & {}>; cipherSuites: string[]; supportedProtocols: string[]; clientAuthentication: \"optional\" | \"none\" | \"required\"; }>; readonly port: number; readonly compression: Readonly<{ referrerWhitelist?: string[] | undefined; } & { enabled: boolean; }>; readonly cors: Readonly<{} & { enabled: boolean; allowCredentials: boolean; allowOrigin: string[] | \"*\"[]; }>; readonly autoListen: boolean; readonly shutdownTimeout: moment.Duration; readonly securityResponseHeaders: Readonly<{} & { referrerPolicy: \"origin\" | \"no-referrer\" | \"no-referrer-when-downgrade\" | \"origin-when-cross-origin\" | \"same-origin\" | \"strict-origin\" | \"strict-origin-when-cross-origin\" | \"unsafe-url\" | null; disableEmbedding: boolean; strictTransportSecurity: string | null; xContentTypeOptions: \"nosniff\" | null; permissionsPolicy: string | null; }>; readonly customResponseHeaders: Record; readonly maxPayload: ", + "{ readonly basePath?: string | undefined; readonly uuid?: string | undefined; readonly publicBaseUrl?: string | undefined; readonly host: string; readonly name: string; readonly compression: Readonly<{ referrerWhitelist?: string[] | undefined; } & { enabled: boolean; }>; readonly ssl: Readonly<{ key?: string | undefined; certificateAuthorities?: string | string[] | undefined; certificate?: string | undefined; keyPassphrase?: string | undefined; redirectHttpFromPort?: number | undefined; } & { enabled: boolean; keystore: Readonly<{ path?: string | undefined; password?: string | undefined; } & {}>; truststore: Readonly<{ path?: string | undefined; password?: string | undefined; } & {}>; cipherSuites: string[]; supportedProtocols: string[]; clientAuthentication: \"optional\" | \"none\" | \"required\"; }>; readonly port: number; readonly cors: Readonly<{} & { enabled: boolean; allowCredentials: boolean; allowOrigin: string[] | \"*\"[]; }>; readonly autoListen: boolean; readonly shutdownTimeout: moment.Duration; readonly securityResponseHeaders: Readonly<{} & { referrerPolicy: \"origin\" | \"no-referrer\" | \"no-referrer-when-downgrade\" | \"origin-when-cross-origin\" | \"same-origin\" | \"strict-origin\" | \"strict-origin-when-cross-origin\" | \"unsafe-url\" | null; disableEmbedding: boolean; strictTransportSecurity: string | null; xContentTypeOptions: \"nosniff\" | null; permissionsPolicy: string | null; }>; readonly customResponseHeaders: Record; readonly maxPayload: ", "ByteSizeValue", "; readonly rewriteBasePath: boolean; readonly keepaliveTimeout: number; readonly socketTimeout: number; readonly xsrf: Readonly<{} & { disableProtection: boolean; allowlist: string[]; }>; readonly requestId: Readonly<{} & { allowFromAnyIp: boolean; ipAllowlist: string[]; }>; }" ], diff --git a/api_docs/kbn_core_http_server_internal.mdx b/api_docs/kbn_core_http_server_internal.mdx index 8c1ce95e5da381..d28a9fb8663685 100644 --- a/api_docs/kbn_core_http_server_internal.mdx +++ b/api_docs/kbn_core_http_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server-internal title: "@kbn/core-http-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-internal'] --- import kbnCoreHttpServerInternalObj from './kbn_core_http_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_server_mocks.mdx b/api_docs/kbn_core_http_server_mocks.mdx index 9855a467f8ff3a..ab6ebf2a1d0aea 100644 --- a/api_docs/kbn_core_http_server_mocks.mdx +++ b/api_docs/kbn_core_http_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server-mocks title: "@kbn/core-http-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-mocks'] --- import kbnCoreHttpServerMocksObj from './kbn_core_http_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_browser.mdx b/api_docs/kbn_core_i18n_browser.mdx index e10bab2bb0ef1d..f4b1fc3af5e3b7 100644 --- a/api_docs/kbn_core_i18n_browser.mdx +++ b/api_docs/kbn_core_i18n_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-browser title: "@kbn/core-i18n-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-browser plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser'] --- import kbnCoreI18nBrowserObj from './kbn_core_i18n_browser.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_browser_mocks.mdx b/api_docs/kbn_core_i18n_browser_mocks.mdx index 6bbcf9a1ed58f1..64528c70bb8a2d 100644 --- a/api_docs/kbn_core_i18n_browser_mocks.mdx +++ b/api_docs/kbn_core_i18n_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-browser-mocks title: "@kbn/core-i18n-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-browser-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser-mocks'] --- import kbnCoreI18nBrowserMocksObj from './kbn_core_i18n_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server.mdx b/api_docs/kbn_core_i18n_server.mdx index ca0b28fad3b83b..66a1608cb08101 100644 --- a/api_docs/kbn_core_i18n_server.mdx +++ b/api_docs/kbn_core_i18n_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server title: "@kbn/core-i18n-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server'] --- import kbnCoreI18nServerObj from './kbn_core_i18n_server.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server_internal.mdx b/api_docs/kbn_core_i18n_server_internal.mdx index d0b3e5f8750ae1..243745c5a4aa7d 100644 --- a/api_docs/kbn_core_i18n_server_internal.mdx +++ b/api_docs/kbn_core_i18n_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server-internal title: "@kbn/core-i18n-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server-internal'] --- import kbnCoreI18nServerInternalObj from './kbn_core_i18n_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server_mocks.mdx b/api_docs/kbn_core_i18n_server_mocks.mdx index 09595678ffd95d..be22af3767c19b 100644 --- a/api_docs/kbn_core_i18n_server_mocks.mdx +++ b/api_docs/kbn_core_i18n_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server-mocks title: "@kbn/core-i18n-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server-mocks'] --- import kbnCoreI18nServerMocksObj from './kbn_core_i18n_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_injected_metadata_browser.mdx b/api_docs/kbn_core_injected_metadata_browser.mdx index f98bc4b4caa51a..53854341a5df01 100644 --- a/api_docs/kbn_core_injected_metadata_browser.mdx +++ b/api_docs/kbn_core_injected_metadata_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-injected-metadata-browser title: "@kbn/core-injected-metadata-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-injected-metadata-browser plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-injected-metadata-browser'] --- import kbnCoreInjectedMetadataBrowserObj from './kbn_core_injected_metadata_browser.devdocs.json'; diff --git a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx index 2d6f60b30b0f4b..d06b1e26a74b5c 100644 --- a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx +++ b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-injected-metadata-browser-mocks title: "@kbn/core-injected-metadata-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-injected-metadata-browser-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-injected-metadata-browser-mocks'] --- import kbnCoreInjectedMetadataBrowserMocksObj from './kbn_core_injected_metadata_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_integrations_browser_internal.mdx b/api_docs/kbn_core_integrations_browser_internal.mdx index 7d6d538a4a3453..a36376c797a837 100644 --- a/api_docs/kbn_core_integrations_browser_internal.mdx +++ b/api_docs/kbn_core_integrations_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-integrations-browser-internal title: "@kbn/core-integrations-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-integrations-browser-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-integrations-browser-internal'] --- import kbnCoreIntegrationsBrowserInternalObj from './kbn_core_integrations_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_integrations_browser_mocks.mdx b/api_docs/kbn_core_integrations_browser_mocks.mdx index 965b44c3d7f494..d6561b9f429999 100644 --- a/api_docs/kbn_core_integrations_browser_mocks.mdx +++ b/api_docs/kbn_core_integrations_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-integrations-browser-mocks title: "@kbn/core-integrations-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-integrations-browser-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-integrations-browser-mocks'] --- import kbnCoreIntegrationsBrowserMocksObj from './kbn_core_integrations_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server.mdx b/api_docs/kbn_core_logging_server.mdx index baf526633cb7fa..4d90b8f49b04f0 100644 --- a/api_docs/kbn_core_logging_server.mdx +++ b/api_docs/kbn_core_logging_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server title: "@kbn/core-logging-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server'] --- import kbnCoreLoggingServerObj from './kbn_core_logging_server.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server_internal.mdx b/api_docs/kbn_core_logging_server_internal.mdx index c7e3e793271dc5..e3e741327ad66c 100644 --- a/api_docs/kbn_core_logging_server_internal.mdx +++ b/api_docs/kbn_core_logging_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-internal title: "@kbn/core-logging-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-internal'] --- import kbnCoreLoggingServerInternalObj from './kbn_core_logging_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server_mocks.mdx b/api_docs/kbn_core_logging_server_mocks.mdx index ba256a3dc7685e..ed80c3b83ee3be 100644 --- a/api_docs/kbn_core_logging_server_mocks.mdx +++ b/api_docs/kbn_core_logging_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-mocks title: "@kbn/core-logging-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-mocks'] --- import kbnCoreLoggingServerMocksObj from './kbn_core_logging_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_collectors_server_internal.mdx b/api_docs/kbn_core_metrics_collectors_server_internal.mdx index 20ddad2be7f374..e6b38a10228c2d 100644 --- a/api_docs/kbn_core_metrics_collectors_server_internal.mdx +++ b/api_docs/kbn_core_metrics_collectors_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-collectors-server-internal title: "@kbn/core-metrics-collectors-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-collectors-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-collectors-server-internal'] --- import kbnCoreMetricsCollectorsServerInternalObj from './kbn_core_metrics_collectors_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_collectors_server_mocks.mdx b/api_docs/kbn_core_metrics_collectors_server_mocks.mdx index 6278558d4a15bd..30610a888ab241 100644 --- a/api_docs/kbn_core_metrics_collectors_server_mocks.mdx +++ b/api_docs/kbn_core_metrics_collectors_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-collectors-server-mocks title: "@kbn/core-metrics-collectors-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-collectors-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-collectors-server-mocks'] --- import kbnCoreMetricsCollectorsServerMocksObj from './kbn_core_metrics_collectors_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server.mdx b/api_docs/kbn_core_metrics_server.mdx index 774dbb4aa58f22..c1849c373ccb92 100644 --- a/api_docs/kbn_core_metrics_server.mdx +++ b/api_docs/kbn_core_metrics_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server title: "@kbn/core-metrics-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server'] --- import kbnCoreMetricsServerObj from './kbn_core_metrics_server.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server_internal.mdx b/api_docs/kbn_core_metrics_server_internal.mdx index b68f29089b3c5e..1ac0d920214c93 100644 --- a/api_docs/kbn_core_metrics_server_internal.mdx +++ b/api_docs/kbn_core_metrics_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server-internal title: "@kbn/core-metrics-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server-internal'] --- import kbnCoreMetricsServerInternalObj from './kbn_core_metrics_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server_mocks.mdx b/api_docs/kbn_core_metrics_server_mocks.mdx index 9da56c2b13d540..ebb4f1cacb5daf 100644 --- a/api_docs/kbn_core_metrics_server_mocks.mdx +++ b/api_docs/kbn_core_metrics_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server-mocks title: "@kbn/core-metrics-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server-mocks'] --- import kbnCoreMetricsServerMocksObj from './kbn_core_metrics_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_mount_utils_browser.mdx b/api_docs/kbn_core_mount_utils_browser.mdx index fc5ad5407ea58e..9a844086577223 100644 --- a/api_docs/kbn_core_mount_utils_browser.mdx +++ b/api_docs/kbn_core_mount_utils_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-mount-utils-browser title: "@kbn/core-mount-utils-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-mount-utils-browser plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-mount-utils-browser'] --- import kbnCoreMountUtilsBrowserObj from './kbn_core_mount_utils_browser.devdocs.json'; diff --git a/api_docs/kbn_core_node_server.mdx b/api_docs/kbn_core_node_server.mdx index cbc248a380a6eb..f1751fe5cf10a9 100644 --- a/api_docs/kbn_core_node_server.mdx +++ b/api_docs/kbn_core_node_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server title: "@kbn/core-node-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server'] --- import kbnCoreNodeServerObj from './kbn_core_node_server.devdocs.json'; diff --git a/api_docs/kbn_core_node_server_internal.mdx b/api_docs/kbn_core_node_server_internal.mdx index 6c389f91fb1dee..9fee3f506b138c 100644 --- a/api_docs/kbn_core_node_server_internal.mdx +++ b/api_docs/kbn_core_node_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server-internal title: "@kbn/core-node-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server-internal'] --- import kbnCoreNodeServerInternalObj from './kbn_core_node_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_node_server_mocks.mdx b/api_docs/kbn_core_node_server_mocks.mdx index e8223e2b459771..fe4faca15de2ae 100644 --- a/api_docs/kbn_core_node_server_mocks.mdx +++ b/api_docs/kbn_core_node_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server-mocks title: "@kbn/core-node-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server-mocks'] --- import kbnCoreNodeServerMocksObj from './kbn_core_node_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser.mdx b/api_docs/kbn_core_notifications_browser.mdx index 2c27b78aff6bb7..e8aaa49807c241 100644 --- a/api_docs/kbn_core_notifications_browser.mdx +++ b/api_docs/kbn_core_notifications_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser title: "@kbn/core-notifications-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser'] --- import kbnCoreNotificationsBrowserObj from './kbn_core_notifications_browser.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser_internal.mdx b/api_docs/kbn_core_notifications_browser_internal.mdx index 197b80b4ba6304..100bb26238b1f9 100644 --- a/api_docs/kbn_core_notifications_browser_internal.mdx +++ b/api_docs/kbn_core_notifications_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser-internal title: "@kbn/core-notifications-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser-internal'] --- import kbnCoreNotificationsBrowserInternalObj from './kbn_core_notifications_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser_mocks.mdx b/api_docs/kbn_core_notifications_browser_mocks.mdx index bf9b1d6b28607d..0efca04f67fe87 100644 --- a/api_docs/kbn_core_notifications_browser_mocks.mdx +++ b/api_docs/kbn_core_notifications_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser-mocks title: "@kbn/core-notifications-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser-mocks'] --- import kbnCoreNotificationsBrowserMocksObj from './kbn_core_notifications_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser.mdx b/api_docs/kbn_core_overlays_browser.mdx index 21561dfeb133d9..3e82839cdc32c0 100644 --- a/api_docs/kbn_core_overlays_browser.mdx +++ b/api_docs/kbn_core_overlays_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser title: "@kbn/core-overlays-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser'] --- import kbnCoreOverlaysBrowserObj from './kbn_core_overlays_browser.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser_internal.mdx b/api_docs/kbn_core_overlays_browser_internal.mdx index d4790272c355d5..be5e68dd816ead 100644 --- a/api_docs/kbn_core_overlays_browser_internal.mdx +++ b/api_docs/kbn_core_overlays_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser-internal title: "@kbn/core-overlays-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser-internal'] --- import kbnCoreOverlaysBrowserInternalObj from './kbn_core_overlays_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser_mocks.mdx b/api_docs/kbn_core_overlays_browser_mocks.mdx index 1e44984b977aef..eca844360edd82 100644 --- a/api_docs/kbn_core_overlays_browser_mocks.mdx +++ b/api_docs/kbn_core_overlays_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser-mocks title: "@kbn/core-overlays-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser-mocks'] --- import kbnCoreOverlaysBrowserMocksObj from './kbn_core_overlays_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server.mdx b/api_docs/kbn_core_preboot_server.mdx index 064c1a7409c91d..83a9a80e5f072e 100644 --- a/api_docs/kbn_core_preboot_server.mdx +++ b/api_docs/kbn_core_preboot_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-preboot-server title: "@kbn/core-preboot-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-preboot-server plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-preboot-server'] --- import kbnCorePrebootServerObj from './kbn_core_preboot_server.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server_mocks.mdx b/api_docs/kbn_core_preboot_server_mocks.mdx index 68d8ba8c77e35a..d2177a367dff0e 100644 --- a/api_docs/kbn_core_preboot_server_mocks.mdx +++ b/api_docs/kbn_core_preboot_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-preboot-server-mocks title: "@kbn/core-preboot-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-preboot-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-preboot-server-mocks'] --- import kbnCorePrebootServerMocksObj from './kbn_core_preboot_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_browser_mocks.mdx b/api_docs/kbn_core_rendering_browser_mocks.mdx index 33c45bca370dbd..7a8b9300bc12dd 100644 --- a/api_docs/kbn_core_rendering_browser_mocks.mdx +++ b/api_docs/kbn_core_rendering_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-browser-mocks title: "@kbn/core-rendering-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-browser-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-browser-mocks'] --- import kbnCoreRenderingBrowserMocksObj from './kbn_core_rendering_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_browser.mdx b/api_docs/kbn_core_saved_objects_api_browser.mdx index 8fb776cf682400..e02802e6bbc193 100644 --- a/api_docs/kbn_core_saved_objects_api_browser.mdx +++ b/api_docs/kbn_core_saved_objects_api_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-browser title: "@kbn/core-saved-objects-api-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-browser plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-browser'] --- import kbnCoreSavedObjectsApiBrowserObj from './kbn_core_saved_objects_api_browser.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server.devdocs.json b/api_docs/kbn_core_saved_objects_api_server.devdocs.json index 0ff68d0fc7bda8..74d6732ba03ba5 100644 --- a/api_docs/kbn_core_saved_objects_api_server.devdocs.json +++ b/api_docs/kbn_core_saved_objects_api_server.devdocs.json @@ -2391,7 +2391,7 @@ "tags": [], "label": "attributes", "description": [ - "{@inheritdoc SavedObjectAttributes}" + "The data for a Saved Object is stored as an object in the `attributes` property." ], "signature": [ "{ [P in keyof T]?: T[P] | undefined; }" diff --git a/api_docs/kbn_core_saved_objects_api_server.mdx b/api_docs/kbn_core_saved_objects_api_server.mdx index 37b831645698d9..8cc05ce3867f0c 100644 --- a/api_docs/kbn_core_saved_objects_api_server.mdx +++ b/api_docs/kbn_core_saved_objects_api_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server title: "@kbn/core-saved-objects-api-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server'] --- import kbnCoreSavedObjectsApiServerObj from './kbn_core_saved_objects_api_server.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server_internal.mdx b/api_docs/kbn_core_saved_objects_api_server_internal.mdx index b1e0f26591dea8..0aa437b82549c7 100644 --- a/api_docs/kbn_core_saved_objects_api_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_api_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server-internal title: "@kbn/core-saved-objects-api-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server-internal'] --- import kbnCoreSavedObjectsApiServerInternalObj from './kbn_core_saved_objects_api_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server_mocks.mdx b/api_docs/kbn_core_saved_objects_api_server_mocks.mdx index 76e68572653434..73c2f84f289c0d 100644 --- a/api_docs/kbn_core_saved_objects_api_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_api_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server-mocks title: "@kbn/core-saved-objects-api-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server-mocks'] --- import kbnCoreSavedObjectsApiServerMocksObj from './kbn_core_saved_objects_api_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_base_server_internal.mdx b/api_docs/kbn_core_saved_objects_base_server_internal.mdx index b0656d2d7698c9..09fd1a2523975e 100644 --- a/api_docs/kbn_core_saved_objects_base_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_base_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-base-server-internal title: "@kbn/core-saved-objects-base-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-base-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-base-server-internal'] --- import kbnCoreSavedObjectsBaseServerInternalObj from './kbn_core_saved_objects_base_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_base_server_mocks.mdx b/api_docs/kbn_core_saved_objects_base_server_mocks.mdx index ecd8eb6b89e587..09e368b16c027d 100644 --- a/api_docs/kbn_core_saved_objects_base_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_base_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-base-server-mocks title: "@kbn/core-saved-objects-base-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-base-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-base-server-mocks'] --- import kbnCoreSavedObjectsBaseServerMocksObj from './kbn_core_saved_objects_base_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser.mdx b/api_docs/kbn_core_saved_objects_browser.mdx index 3095a10b0e8a37..1225cd66a4a023 100644 --- a/api_docs/kbn_core_saved_objects_browser.mdx +++ b/api_docs/kbn_core_saved_objects_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser title: "@kbn/core-saved-objects-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser'] --- import kbnCoreSavedObjectsBrowserObj from './kbn_core_saved_objects_browser.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser_internal.mdx b/api_docs/kbn_core_saved_objects_browser_internal.mdx index 6e6235f54d936e..9d0b9bd4cb5cdc 100644 --- a/api_docs/kbn_core_saved_objects_browser_internal.mdx +++ b/api_docs/kbn_core_saved_objects_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser-internal title: "@kbn/core-saved-objects-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser-internal'] --- import kbnCoreSavedObjectsBrowserInternalObj from './kbn_core_saved_objects_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser_mocks.mdx b/api_docs/kbn_core_saved_objects_browser_mocks.mdx index 9ba194c06f4bd5..85c89f18ea0a3a 100644 --- a/api_docs/kbn_core_saved_objects_browser_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser-mocks title: "@kbn/core-saved-objects-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser-mocks'] --- import kbnCoreSavedObjectsBrowserMocksObj from './kbn_core_saved_objects_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_common.devdocs.json b/api_docs/kbn_core_saved_objects_common.devdocs.json index f6d379e5699ac1..78a36566609fa4 100644 --- a/api_docs/kbn_core_saved_objects_common.devdocs.json +++ b/api_docs/kbn_core_saved_objects_common.devdocs.json @@ -127,7 +127,7 @@ "tags": [], "label": "attributes", "description": [ - "{@inheritdoc SavedObjectAttributes}" + "The data for a Saved Object is stored as an object in the `attributes` property." ], "signature": [ "T" @@ -237,14 +237,17 @@ "parentPluginId": "@kbn/core-saved-objects-common", "id": "def-common.SavedObjectAttributes", "type": "Interface", - "tags": [], + "tags": [ + "deprecated" + ], "label": "SavedObjectAttributes", "description": [ "\nThe data for a Saved Object is stored as an object in the `attributes`\nproperty.\n" ], "path": "packages/core/saved-objects/core-saved-objects-common/src/saved_objects.ts", - "deprecated": false, + "deprecated": true, "trackAdoption": false, + "references": [], "children": [ { "parentPluginId": "@kbn/core-saved-objects-common", diff --git a/api_docs/kbn_core_saved_objects_common.mdx b/api_docs/kbn_core_saved_objects_common.mdx index 841d63be63be47..fa224db4202915 100644 --- a/api_docs/kbn_core_saved_objects_common.mdx +++ b/api_docs/kbn_core_saved_objects_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-common title: "@kbn/core-saved-objects-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-common plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-common'] --- import kbnCoreSavedObjectsCommonObj from './kbn_core_saved_objects_common.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx b/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx index 62ca0892a8ef0f..ac27e89b306e17 100644 --- a/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-import-export-server-internal title: "@kbn/core-saved-objects-import-export-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-import-export-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-import-export-server-internal'] --- import kbnCoreSavedObjectsImportExportServerInternalObj from './kbn_core_saved_objects_import_export_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx b/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx index 6db912ca3d51da..211f344e7c14d1 100644 --- a/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-import-export-server-mocks title: "@kbn/core-saved-objects-import-export-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-import-export-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-import-export-server-mocks'] --- import kbnCoreSavedObjectsImportExportServerMocksObj from './kbn_core_saved_objects_import_export_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_migration_server_internal.mdx b/api_docs/kbn_core_saved_objects_migration_server_internal.mdx index 124ba03f2d7cb0..a9f5fa3280a152 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_migration_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-migration-server-internal title: "@kbn/core-saved-objects-migration-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-migration-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-migration-server-internal'] --- import kbnCoreSavedObjectsMigrationServerInternalObj from './kbn_core_saved_objects_migration_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx b/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx index 436795ec64440f..a13b7cdb36c5ce 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-migration-server-mocks title: "@kbn/core-saved-objects-migration-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-migration-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-migration-server-mocks'] --- import kbnCoreSavedObjectsMigrationServerMocksObj from './kbn_core_saved_objects_migration_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server.devdocs.json b/api_docs/kbn_core_saved_objects_server.devdocs.json index 3ab55e6ecc442c..f21f0a648a3910 100644 --- a/api_docs/kbn_core_saved_objects_server.devdocs.json +++ b/api_docs/kbn_core_saved_objects_server.devdocs.json @@ -2754,9 +2754,7 @@ "\nRegister a {@link SavedObjectsType | savedObjects type} definition.\n\nSee the {@link SavedObjectsTypeMappingDefinition | mappings format} and\n{@link SavedObjectMigrationMap | migration format} for more details about these.\n" ], "signature": [ - "(type: ", + "(type: ", { "pluginId": "@kbn/core-saved-objects-server", "scope": "server", diff --git a/api_docs/kbn_core_saved_objects_server.mdx b/api_docs/kbn_core_saved_objects_server.mdx index d39b682d0e9227..2bd86c24020aff 100644 --- a/api_docs/kbn_core_saved_objects_server.mdx +++ b/api_docs/kbn_core_saved_objects_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server title: "@kbn/core-saved-objects-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server'] --- import kbnCoreSavedObjectsServerObj from './kbn_core_saved_objects_server.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server_internal.mdx b/api_docs/kbn_core_saved_objects_server_internal.mdx index 097b612967b60f..16e2436cf9255a 100644 --- a/api_docs/kbn_core_saved_objects_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server-internal title: "@kbn/core-saved-objects-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server-internal'] --- import kbnCoreSavedObjectsServerInternalObj from './kbn_core_saved_objects_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server_mocks.mdx b/api_docs/kbn_core_saved_objects_server_mocks.mdx index 5cfc809e628d45..cd9b3ddac55f14 100644 --- a/api_docs/kbn_core_saved_objects_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server-mocks title: "@kbn/core-saved-objects-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server-mocks'] --- import kbnCoreSavedObjectsServerMocksObj from './kbn_core_saved_objects_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_utils_server.mdx b/api_docs/kbn_core_saved_objects_utils_server.mdx index 0735565b60ba30..fc9d2ee55ff1bf 100644 --- a/api_docs/kbn_core_saved_objects_utils_server.mdx +++ b/api_docs/kbn_core_saved_objects_utils_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-utils-server title: "@kbn/core-saved-objects-utils-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-utils-server plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-utils-server'] --- import kbnCoreSavedObjectsUtilsServerObj from './kbn_core_saved_objects_utils_server.devdocs.json'; diff --git a/api_docs/kbn_core_status_common.devdocs.json b/api_docs/kbn_core_status_common.devdocs.json new file mode 100644 index 00000000000000..b49607b86963e8 --- /dev/null +++ b/api_docs/kbn_core_status_common.devdocs.json @@ -0,0 +1,242 @@ +{ + "id": "@kbn/core-status-common", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [ + { + "parentPluginId": "@kbn/core-status-common", + "id": "def-common.CoreStatus", + "type": "Interface", + "tags": [], + "label": "CoreStatus", + "description": [ + "\nStatus of core services.\n" + ], + "path": "packages/core/status/core-status-common/src/core_status.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-status-common", + "id": "def-common.CoreStatus.elasticsearch", + "type": "Object", + "tags": [], + "label": "elasticsearch", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-status-common", + "scope": "common", + "docId": "kibKbnCoreStatusCommonPluginApi", + "section": "def-common.ServiceStatus", + "text": "ServiceStatus" + }, + "" + ], + "path": "packages/core/status/core-status-common/src/core_status.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-common", + "id": "def-common.CoreStatus.savedObjects", + "type": "Object", + "tags": [], + "label": "savedObjects", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-status-common", + "scope": "common", + "docId": "kibKbnCoreStatusCommonPluginApi", + "section": "def-common.ServiceStatus", + "text": "ServiceStatus" + }, + "" + ], + "path": "packages/core/status/core-status-common/src/core_status.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-status-common", + "id": "def-common.ServiceStatus", + "type": "Interface", + "tags": [], + "label": "ServiceStatus", + "description": [ + "\nThe current status of a service at a point in time.\n" + ], + "signature": [ + { + "pluginId": "@kbn/core-status-common", + "scope": "common", + "docId": "kibKbnCoreStatusCommonPluginApi", + "section": "def-common.ServiceStatus", + "text": "ServiceStatus" + }, + "" + ], + "path": "packages/core/status/core-status-common/src/service_status.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-status-common", + "id": "def-common.ServiceStatus.level", + "type": "CompoundType", + "tags": [], + "label": "level", + "description": [ + "\nThe current availability level of the service." + ], + "signature": [ + "Readonly<{ toString: () => \"available\"; valueOf: () => 0; toJSON: () => \"available\"; }> | Readonly<{ toString: () => \"degraded\"; valueOf: () => 1; toJSON: () => \"degraded\"; }> | Readonly<{ toString: () => \"unavailable\"; valueOf: () => 2; toJSON: () => \"unavailable\"; }> | Readonly<{ toString: () => \"critical\"; valueOf: () => 3; toJSON: () => \"critical\"; }>" + ], + "path": "packages/core/status/core-status-common/src/service_status.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-common", + "id": "def-common.ServiceStatus.summary", + "type": "string", + "tags": [], + "label": "summary", + "description": [ + "\nA high-level summary of the service status." + ], + "path": "packages/core/status/core-status-common/src/service_status.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-common", + "id": "def-common.ServiceStatus.detail", + "type": "string", + "tags": [], + "label": "detail", + "description": [ + "\nA more detailed description of the service status." + ], + "signature": [ + "string | undefined" + ], + "path": "packages/core/status/core-status-common/src/service_status.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-common", + "id": "def-common.ServiceStatus.documentationUrl", + "type": "string", + "tags": [], + "label": "documentationUrl", + "description": [ + "\nA URL to open in a new tab about how to resolve or troubleshoot the problem." + ], + "signature": [ + "string | undefined" + ], + "path": "packages/core/status/core-status-common/src/service_status.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-common", + "id": "def-common.ServiceStatus.meta", + "type": "Uncategorized", + "tags": [], + "label": "meta", + "description": [ + "\nAny JSON-serializable data to be included in the HTTP API response. Useful for providing more fine-grained,\nmachine-readable information about the service status. May include status information for underlying features." + ], + "signature": [ + "Meta | undefined" + ], + "path": "packages/core/status/core-status-common/src/service_status.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], + "enums": [], + "misc": [ + { + "parentPluginId": "@kbn/core-status-common", + "id": "def-common.ServiceStatusLevel", + "type": "Type", + "tags": [], + "label": "ServiceStatusLevel", + "description": [ + "\nA convenience type that represents the union of each value in {@link ServiceStatusLevels}." + ], + "signature": [ + "Readonly<{ toString: () => \"available\"; valueOf: () => 0; toJSON: () => \"available\"; }> | Readonly<{ toString: () => \"degraded\"; valueOf: () => 1; toJSON: () => \"degraded\"; }> | Readonly<{ toString: () => \"unavailable\"; valueOf: () => 2; toJSON: () => \"unavailable\"; }> | Readonly<{ toString: () => \"critical\"; valueOf: () => 3; toJSON: () => \"critical\"; }>" + ], + "path": "packages/core/status/core-status-common/src/service_status.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-status-common", + "id": "def-common.ServiceStatusLevelId", + "type": "Type", + "tags": [], + "label": "ServiceStatusLevelId", + "description": [ + "\nPossible values for the ID of a {@link ServiceStatusLevel}\n" + ], + "signature": [ + "\"critical\" | \"degraded\" | \"unavailable\" | \"available\"" + ], + "path": "packages/core/status/core-status-common/src/service_status.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [ + { + "parentPluginId": "@kbn/core-status-common", + "id": "def-common.ServiceStatusLevels", + "type": "Object", + "tags": [], + "label": "ServiceStatusLevels", + "description": [ + "\nThe current \"level\" of availability of a service.\n" + ], + "signature": [ + "{ readonly available: Readonly<{ toString: () => \"available\"; valueOf: () => 0; toJSON: () => \"available\"; }>; readonly degraded: Readonly<{ toString: () => \"degraded\"; valueOf: () => 1; toJSON: () => \"degraded\"; }>; readonly unavailable: Readonly<{ toString: () => \"unavailable\"; valueOf: () => 2; toJSON: () => \"unavailable\"; }>; readonly critical: Readonly<{ toString: () => \"critical\"; valueOf: () => 3; toJSON: () => \"critical\"; }>; }" + ], + "path": "packages/core/status/core-status-common/src/service_status.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ] + } +} \ No newline at end of file diff --git a/api_docs/kbn_core_status_common.mdx b/api_docs/kbn_core_status_common.mdx new file mode 100644 index 00000000000000..fce1412e287b8c --- /dev/null +++ b/api_docs/kbn_core_status_common.mdx @@ -0,0 +1,36 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnCoreStatusCommonPluginApi +slug: /kibana-dev-docs/api/kbn-core-status-common +title: "@kbn/core-status-common" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/core-status-common plugin +date: 2022-09-14 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common'] +--- +import kbnCoreStatusCommonObj from './kbn_core_status_common.devdocs.json'; + + + +Contact Kibana Core for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 12 | 0 | 2 | 0 | + +## Common + +### Objects + + +### Interfaces + + +### Consts, variables and types + + diff --git a/api_docs/kbn_core_status_common_internal.devdocs.json b/api_docs/kbn_core_status_common_internal.devdocs.json new file mode 100644 index 00000000000000..39b4d63b9e25a9 --- /dev/null +++ b/api_docs/kbn_core_status_common_internal.devdocs.json @@ -0,0 +1,355 @@ +{ + "id": "@kbn/core-status-common-internal", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [ + { + "parentPluginId": "@kbn/core-status-common-internal", + "id": "def-common.ServerVersion", + "type": "Interface", + "tags": [], + "label": "ServerVersion", + "description": [], + "path": "packages/core/status/core-status-common-internal/src/status.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-status-common-internal", + "id": "def-common.ServerVersion.number", + "type": "string", + "tags": [], + "label": "number", + "description": [], + "path": "packages/core/status/core-status-common-internal/src/status.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-common-internal", + "id": "def-common.ServerVersion.build_hash", + "type": "string", + "tags": [], + "label": "build_hash", + "description": [], + "path": "packages/core/status/core-status-common-internal/src/status.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-common-internal", + "id": "def-common.ServerVersion.build_number", + "type": "number", + "tags": [], + "label": "build_number", + "description": [], + "path": "packages/core/status/core-status-common-internal/src/status.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-common-internal", + "id": "def-common.ServerVersion.build_snapshot", + "type": "boolean", + "tags": [], + "label": "build_snapshot", + "description": [], + "path": "packages/core/status/core-status-common-internal/src/status.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-status-common-internal", + "id": "def-common.StatusInfo", + "type": "Interface", + "tags": [], + "label": "StatusInfo", + "description": [], + "path": "packages/core/status/core-status-common-internal/src/status.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-status-common-internal", + "id": "def-common.StatusInfo.overall", + "type": "Object", + "tags": [], + "label": "overall", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-status-common-internal", + "scope": "common", + "docId": "kibKbnCoreStatusCommonInternalPluginApi", + "section": "def-common.StatusInfoServiceStatus", + "text": "StatusInfoServiceStatus" + } + ], + "path": "packages/core/status/core-status-common-internal/src/status.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-common-internal", + "id": "def-common.StatusInfo.core", + "type": "Object", + "tags": [], + "label": "core", + "description": [], + "signature": [ + "{ elasticsearch: ", + { + "pluginId": "@kbn/core-status-common-internal", + "scope": "common", + "docId": "kibKbnCoreStatusCommonInternalPluginApi", + "section": "def-common.StatusInfoServiceStatus", + "text": "StatusInfoServiceStatus" + }, + "; savedObjects: ", + { + "pluginId": "@kbn/core-status-common-internal", + "scope": "common", + "docId": "kibKbnCoreStatusCommonInternalPluginApi", + "section": "def-common.StatusInfoServiceStatus", + "text": "StatusInfoServiceStatus" + }, + "; }" + ], + "path": "packages/core/status/core-status-common-internal/src/status.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-common-internal", + "id": "def-common.StatusInfo.plugins", + "type": "Object", + "tags": [], + "label": "plugins", + "description": [], + "signature": [ + "{ [x: string]: ", + { + "pluginId": "@kbn/core-status-common-internal", + "scope": "common", + "docId": "kibKbnCoreStatusCommonInternalPluginApi", + "section": "def-common.StatusInfoServiceStatus", + "text": "StatusInfoServiceStatus" + }, + "; }" + ], + "path": "packages/core/status/core-status-common-internal/src/status.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-status-common-internal", + "id": "def-common.StatusInfoServiceStatus", + "type": "Interface", + "tags": [], + "label": "StatusInfoServiceStatus", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-status-common-internal", + "scope": "common", + "docId": "kibKbnCoreStatusCommonInternalPluginApi", + "section": "def-common.StatusInfoServiceStatus", + "text": "StatusInfoServiceStatus" + }, + " extends Omit<", + "ServiceStatus", + ", \"level\">" + ], + "path": "packages/core/status/core-status-common-internal/src/status.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-status-common-internal", + "id": "def-common.StatusInfoServiceStatus.level", + "type": "CompoundType", + "tags": [], + "label": "level", + "description": [], + "signature": [ + "\"critical\" | \"degraded\" | \"unavailable\" | \"available\"" + ], + "path": "packages/core/status/core-status-common-internal/src/status.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-status-common-internal", + "id": "def-common.StatusResponse", + "type": "Interface", + "tags": [], + "label": "StatusResponse", + "description": [], + "path": "packages/core/status/core-status-common-internal/src/status.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-status-common-internal", + "id": "def-common.StatusResponse.name", + "type": "string", + "tags": [], + "label": "name", + "description": [], + "path": "packages/core/status/core-status-common-internal/src/status.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-common-internal", + "id": "def-common.StatusResponse.uuid", + "type": "string", + "tags": [], + "label": "uuid", + "description": [], + "path": "packages/core/status/core-status-common-internal/src/status.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-common-internal", + "id": "def-common.StatusResponse.version", + "type": "Object", + "tags": [], + "label": "version", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-status-common-internal", + "scope": "common", + "docId": "kibKbnCoreStatusCommonInternalPluginApi", + "section": "def-common.ServerVersion", + "text": "ServerVersion" + } + ], + "path": "packages/core/status/core-status-common-internal/src/status.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-common-internal", + "id": "def-common.StatusResponse.status", + "type": "Object", + "tags": [], + "label": "status", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-status-common-internal", + "scope": "common", + "docId": "kibKbnCoreStatusCommonInternalPluginApi", + "section": "def-common.StatusInfo", + "text": "StatusInfo" + } + ], + "path": "packages/core/status/core-status-common-internal/src/status.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-common-internal", + "id": "def-common.StatusResponse.metrics", + "type": "CompoundType", + "tags": [], + "label": "metrics", + "description": [], + "signature": [ + "Omit<", + "OpsMetrics", + ", \"collected_at\"> & { last_updated: string; collection_interval_in_millis: number; requests: { status_codes: Record; }; }" + ], + "path": "packages/core/status/core-status-common-internal/src/status.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], + "enums": [], + "misc": [ + { + "parentPluginId": "@kbn/core-status-common-internal", + "id": "def-common.ServerMetrics", + "type": "Type", + "tags": [], + "label": "ServerMetrics", + "description": [], + "signature": [ + "Omit<", + "OpsMetrics", + ", \"collected_at\"> & { last_updated: string; collection_interval_in_millis: number; requests: { status_codes: Record; }; }" + ], + "path": "packages/core/status/core-status-common-internal/src/status.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-status-common-internal", + "id": "def-common.StatusInfoCoreStatus", + "type": "Type", + "tags": [], + "label": "StatusInfoCoreStatus", + "description": [ + "\nCopy all the services listed in CoreStatus with their specific ServiceStatus declarations\nbut overwriting the `level` to its stringified version." + ], + "signature": [ + "{ elasticsearch: ", + { + "pluginId": "@kbn/core-status-common-internal", + "scope": "common", + "docId": "kibKbnCoreStatusCommonInternalPluginApi", + "section": "def-common.StatusInfoServiceStatus", + "text": "StatusInfoServiceStatus" + }, + "; savedObjects: ", + { + "pluginId": "@kbn/core-status-common-internal", + "scope": "common", + "docId": "kibKbnCoreStatusCommonInternalPluginApi", + "section": "def-common.StatusInfoServiceStatus", + "text": "StatusInfoServiceStatus" + }, + "; }" + ], + "path": "packages/core/status/core-status-common-internal/src/status.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_core_status_common_internal.mdx b/api_docs/kbn_core_status_common_internal.mdx new file mode 100644 index 00000000000000..d0df3a1490d54c --- /dev/null +++ b/api_docs/kbn_core_status_common_internal.mdx @@ -0,0 +1,33 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnCoreStatusCommonInternalPluginApi +slug: /kibana-dev-docs/api/kbn-core-status-common-internal +title: "@kbn/core-status-common-internal" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/core-status-common-internal plugin +date: 2022-09-14 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common-internal'] +--- +import kbnCoreStatusCommonInternalObj from './kbn_core_status_common_internal.devdocs.json'; + + + +Contact Kibana Core for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 19 | 0 | 18 | 0 | + +## Common + +### Interfaces + + +### Consts, variables and types + + diff --git a/api_docs/kbn_core_status_server.devdocs.json b/api_docs/kbn_core_status_server.devdocs.json new file mode 100644 index 00000000000000..6438e27aa69d3f --- /dev/null +++ b/api_docs/kbn_core_status_server.devdocs.json @@ -0,0 +1,378 @@ +{ + "id": "@kbn/core-status-server", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [ + { + "parentPluginId": "@kbn/core-status-server", + "id": "def-server.CoreStatus", + "type": "Interface", + "tags": [], + "label": "CoreStatus", + "description": [ + "\nStatus of core services.\n" + ], + "signature": [ + "CoreStatus" + ], + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-status-server", + "id": "def-server.CoreStatus.elasticsearch", + "type": "Object", + "tags": [], + "label": "elasticsearch", + "description": [], + "signature": [ + "ServiceStatus", + "" + ], + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-server", + "id": "def-server.CoreStatus.savedObjects", + "type": "Object", + "tags": [], + "label": "savedObjects", + "description": [], + "signature": [ + "ServiceStatus", + "" + ], + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-status-server", + "id": "def-server.ServiceStatus", + "type": "Interface", + "tags": [], + "label": "ServiceStatus", + "description": [ + "\nThe current status of a service at a point in time.\n" + ], + "signature": [ + "ServiceStatus", + "" + ], + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-status-server", + "id": "def-server.ServiceStatus.level", + "type": "CompoundType", + "tags": [], + "label": "level", + "description": [ + "\nThe current availability level of the service." + ], + "signature": [ + "Readonly<{ toString: () => \"available\"; valueOf: () => 0; toJSON: () => \"available\"; }> | Readonly<{ toString: () => \"degraded\"; valueOf: () => 1; toJSON: () => \"degraded\"; }> | Readonly<{ toString: () => \"unavailable\"; valueOf: () => 2; toJSON: () => \"unavailable\"; }> | Readonly<{ toString: () => \"critical\"; valueOf: () => 3; toJSON: () => \"critical\"; }>" + ], + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-server", + "id": "def-server.ServiceStatus.summary", + "type": "string", + "tags": [], + "label": "summary", + "description": [ + "\nA high-level summary of the service status." + ], + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-server", + "id": "def-server.ServiceStatus.detail", + "type": "string", + "tags": [], + "label": "detail", + "description": [ + "\nA more detailed description of the service status." + ], + "signature": [ + "string | undefined" + ], + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-server", + "id": "def-server.ServiceStatus.documentationUrl", + "type": "string", + "tags": [], + "label": "documentationUrl", + "description": [ + "\nA URL to open in a new tab about how to resolve or troubleshoot the problem." + ], + "signature": [ + "string | undefined" + ], + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-server", + "id": "def-server.ServiceStatus.meta", + "type": "Uncategorized", + "tags": [], + "label": "meta", + "description": [ + "\nAny JSON-serializable data to be included in the HTTP API response. Useful for providing more fine-grained,\nmachine-readable information about the service status. May include status information for underlying features." + ], + "signature": [ + "Meta | undefined" + ], + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-status-server", + "id": "def-server.StatusServiceSetup", + "type": "Interface", + "tags": [], + "label": "StatusServiceSetup", + "description": [ + "\nAPI for accessing status of Core and this plugin's dependencies as well as for customizing this plugin's status.\n" + ], + "path": "packages/core/status/core-status-server/src/contracts.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-status-server", + "id": "def-server.StatusServiceSetup.core$", + "type": "Object", + "tags": [], + "label": "core$", + "description": [ + "\nCurrent status for all Core services." + ], + "signature": [ + "Observable", + "<", + "CoreStatus", + ">" + ], + "path": "packages/core/status/core-status-server/src/contracts.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-server", + "id": "def-server.StatusServiceSetup.overall$", + "type": "Object", + "tags": [], + "label": "overall$", + "description": [ + "\nOverall system status for all of Kibana.\n" + ], + "signature": [ + "Observable", + "<", + "ServiceStatus", + ">" + ], + "path": "packages/core/status/core-status-server/src/contracts.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-server", + "id": "def-server.StatusServiceSetup.set", + "type": "Function", + "tags": [], + "label": "set", + "description": [ + "\nAllows a plugin to specify a custom status dependent on its own criteria.\nCompletely overrides the default inherited status.\n" + ], + "signature": [ + "(status$: ", + "Observable", + "<", + "ServiceStatus", + ">) => void" + ], + "path": "packages/core/status/core-status-server/src/contracts.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-status-server", + "id": "def-server.StatusServiceSetup.set.$1", + "type": "Object", + "tags": [], + "label": "status$", + "description": [], + "signature": [ + "Observable", + "<", + "ServiceStatus", + ">" + ], + "path": "packages/core/status/core-status-server/src/contracts.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/core-status-server", + "id": "def-server.StatusServiceSetup.dependencies$", + "type": "Object", + "tags": [], + "label": "dependencies$", + "description": [ + "\nCurrent status for all plugins this plugin depends on.\nEach key of the `Record` is a plugin id." + ], + "signature": [ + "Observable", + ">>" + ], + "path": "packages/core/status/core-status-server/src/contracts.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-server", + "id": "def-server.StatusServiceSetup.derivedStatus$", + "type": "Object", + "tags": [], + "label": "derivedStatus$", + "description": [ + "\nThe status of this plugin as derived from its dependencies.\n" + ], + "signature": [ + "Observable", + "<", + "ServiceStatus", + ">" + ], + "path": "packages/core/status/core-status-server/src/contracts.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-server", + "id": "def-server.StatusServiceSetup.isStatusPageAnonymous", + "type": "Function", + "tags": [], + "label": "isStatusPageAnonymous", + "description": [ + "\nWhether or not the status HTTP APIs are available to unauthenticated users when an authentication provider is\npresent." + ], + "signature": [ + "() => boolean" + ], + "path": "packages/core/status/core-status-server/src/contracts.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + } + ], + "initialIsOpen": false + } + ], + "enums": [], + "misc": [ + { + "parentPluginId": "@kbn/core-status-server", + "id": "def-server.ServiceStatusLevel", + "type": "Type", + "tags": [], + "label": "ServiceStatusLevel", + "description": [ + "\nA convenience type that represents the union of each value in {@link ServiceStatusLevels}." + ], + "signature": [ + "Readonly<{ toString: () => \"available\"; valueOf: () => 0; toJSON: () => \"available\"; }> | Readonly<{ toString: () => \"degraded\"; valueOf: () => 1; toJSON: () => \"degraded\"; }> | Readonly<{ toString: () => \"unavailable\"; valueOf: () => 2; toJSON: () => \"unavailable\"; }> | Readonly<{ toString: () => \"critical\"; valueOf: () => 3; toJSON: () => \"critical\"; }>" + ], + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-status-server", + "id": "def-server.ServiceStatusLevelId", + "type": "Type", + "tags": [], + "label": "ServiceStatusLevelId", + "description": [ + "\nPossible values for the ID of a {@link ServiceStatusLevel}\n" + ], + "signature": [ + "\"critical\" | \"degraded\" | \"unavailable\" | \"available\"" + ], + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [ + { + "parentPluginId": "@kbn/core-status-server", + "id": "def-server.ServiceStatusLevels", + "type": "Object", + "tags": [], + "label": "ServiceStatusLevels", + "description": [ + "\nThe current \"level\" of availability of a service.\n" + ], + "signature": [ + "{ readonly available: Readonly<{ toString: () => \"available\"; valueOf: () => 0; toJSON: () => \"available\"; }>; readonly degraded: Readonly<{ toString: () => \"degraded\"; valueOf: () => 1; toJSON: () => \"degraded\"; }>; readonly unavailable: Readonly<{ toString: () => \"unavailable\"; valueOf: () => 2; toJSON: () => \"unavailable\"; }>; readonly critical: Readonly<{ toString: () => \"critical\"; valueOf: () => 3; toJSON: () => \"critical\"; }>; }" + ], + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_core_status_server.mdx b/api_docs/kbn_core_status_server.mdx new file mode 100644 index 00000000000000..fc96a13ec5b799 --- /dev/null +++ b/api_docs/kbn_core_status_server.mdx @@ -0,0 +1,36 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnCoreStatusServerPluginApi +slug: /kibana-dev-docs/api/kbn-core-status-server +title: "@kbn/core-status-server" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/core-status-server plugin +date: 2022-09-14 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server'] +--- +import kbnCoreStatusServerObj from './kbn_core_status_server.devdocs.json'; + + + +Contact Kibana Core for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 20 | 0 | 1 | 0 | + +## Server + +### Objects + + +### Interfaces + + +### Consts, variables and types + + diff --git a/api_docs/kbn_core_status_server_internal.devdocs.json b/api_docs/kbn_core_status_server_internal.devdocs.json new file mode 100644 index 00000000000000..d24cd2e1438309 --- /dev/null +++ b/api_docs/kbn_core_status_server_internal.devdocs.json @@ -0,0 +1,440 @@ +{ + "id": "@kbn/core-status-server-internal", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [ + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.StatusService", + "type": "Class", + "tags": [], + "label": "StatusService", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-status-server-internal", + "scope": "server", + "docId": "kibKbnCoreStatusServerInternalPluginApi", + "section": "def-server.StatusService", + "text": "StatusService" + }, + " implements ", + "CoreService", + "<", + "InternalStatusServiceSetup", + ", void>" + ], + "path": "packages/core/status/core-status-server-internal/src/status_service.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.StatusService.Unnamed", + "type": "Function", + "tags": [], + "label": "Constructor", + "description": [], + "signature": [ + "any" + ], + "path": "packages/core/status/core-status-server-internal/src/status_service.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.StatusService.Unnamed.$1", + "type": "Object", + "tags": [], + "label": "coreContext", + "description": [], + "signature": [ + "CoreContext" + ], + "path": "packages/core/status/core-status-server-internal/src/status_service.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.StatusService.setup", + "type": "Function", + "tags": [], + "label": "setup", + "description": [], + "signature": [ + "({ analytics, elasticsearch, pluginDependencies, http, metrics, savedObjects, environment, coreUsageData, }: ", + { + "pluginId": "@kbn/core-status-server-internal", + "scope": "server", + "docId": "kibKbnCoreStatusServerInternalPluginApi", + "section": "def-server.StatusServiceSetupDeps", + "text": "StatusServiceSetupDeps" + }, + ") => Promise<{ core$: ", + "Observable", + "<", + "CoreStatus", + ">; coreOverall$: ", + "Observable", + "<", + "ServiceStatus", + ">; overall$: ", + "Observable", + "<", + "ServiceStatus", + ">; plugins: { set: (plugin: string, status$: ", + "Observable", + "<", + "ServiceStatus", + ">) => void; getDependenciesStatus$: (plugin: string) => ", + "Observable", + ">>; getDerivedStatus$: (plugin: string) => ", + "Observable", + "<", + "ServiceStatus", + ">; }; isStatusPageAnonymous: () => boolean; }>" + ], + "path": "packages/core/status/core-status-server-internal/src/status_service.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.StatusService.setup.$1", + "type": "Object", + "tags": [], + "label": "{\n analytics,\n elasticsearch,\n pluginDependencies,\n http,\n metrics,\n savedObjects,\n environment,\n coreUsageData,\n }", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-status-server-internal", + "scope": "server", + "docId": "kibKbnCoreStatusServerInternalPluginApi", + "section": "def-server.StatusServiceSetupDeps", + "text": "StatusServiceSetupDeps" + } + ], + "path": "packages/core/status/core-status-server-internal/src/status_service.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.StatusService.start", + "type": "Function", + "tags": [], + "label": "start", + "description": [], + "signature": [ + "() => void" + ], + "path": "packages/core/status/core-status-server-internal/src/status_service.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.StatusService.stop", + "type": "Function", + "tags": [], + "label": "stop", + "description": [], + "signature": [ + "() => void" + ], + "path": "packages/core/status/core-status-server-internal/src/status_service.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + } + ], + "initialIsOpen": false + } + ], + "functions": [ + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.registerStatusRoute", + "type": "Function", + "tags": [], + "label": "registerStatusRoute", + "description": [], + "signature": [ + "({ router, config, metrics, status, incrementUsageCounter, }: Deps) => void" + ], + "path": "packages/core/status/core-status-server-internal/src/routes/status.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.registerStatusRoute.$1", + "type": "Object", + "tags": [], + "label": "{\n router,\n config,\n metrics,\n status,\n incrementUsageCounter,\n}", + "description": [], + "signature": [ + "Deps" + ], + "path": "packages/core/status/core-status-server-internal/src/routes/status.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [ + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.StatusServiceSetupDeps", + "type": "Interface", + "tags": [], + "label": "StatusServiceSetupDeps", + "description": [], + "path": "packages/core/status/core-status-server-internal/src/status_service.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.StatusServiceSetupDeps.analytics", + "type": "Object", + "tags": [], + "label": "analytics", + "description": [], + "signature": [ + "{ optIn: (optInConfig: ", + "OptInConfig", + ") => void; reportEvent: (eventType: string, eventData: EventTypeData) => void; readonly telemetryCounter$: ", + "Observable", + "<", + "TelemetryCounter", + ">; registerEventType: (eventTypeOps: ", + "EventTypeOpts", + ") => void; registerShipper: (Shipper: ", + "ShipperClassConstructor", + ", shipperConfig: ShipperConfig, opts?: ", + "RegisterShipperOpts", + " | undefined) => void; registerContextProvider: (contextProviderOpts: ", + "ContextProviderOpts", + ") => void; removeContextProvider: (contextProviderName: string) => void; }" + ], + "path": "packages/core/status/core-status-server-internal/src/status_service.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.StatusServiceSetupDeps.elasticsearch", + "type": "Object", + "tags": [], + "label": "elasticsearch", + "description": [], + "signature": [ + "{ status$: ", + "Observable", + "<", + "ServiceStatus", + "<", + "ElasticsearchStatusMeta", + ">>; }" + ], + "path": "packages/core/status/core-status-server-internal/src/status_service.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.StatusServiceSetupDeps.environment", + "type": "Object", + "tags": [], + "label": "environment", + "description": [], + "signature": [ + "InternalEnvironmentServicePreboot" + ], + "path": "packages/core/status/core-status-server-internal/src/status_service.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.StatusServiceSetupDeps.pluginDependencies", + "type": "Object", + "tags": [], + "label": "pluginDependencies", + "description": [], + "signature": [ + "ReadonlyMap" + ], + "path": "packages/core/status/core-status-server-internal/src/status_service.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.StatusServiceSetupDeps.http", + "type": "Object", + "tags": [], + "label": "http", + "description": [], + "signature": [ + "InternalHttpServiceSetup" + ], + "path": "packages/core/status/core-status-server-internal/src/status_service.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.StatusServiceSetupDeps.metrics", + "type": "Object", + "tags": [], + "label": "metrics", + "description": [], + "signature": [ + "MetricsServiceSetup" + ], + "path": "packages/core/status/core-status-server-internal/src/status_service.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.StatusServiceSetupDeps.savedObjects", + "type": "Object", + "tags": [], + "label": "savedObjects", + "description": [], + "signature": [ + "{ status$: ", + "Observable", + "<", + "ServiceStatus", + "<", + "SavedObjectStatusMeta", + ">>; }" + ], + "path": "packages/core/status/core-status-server-internal/src/status_service.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.StatusServiceSetupDeps.coreUsageData", + "type": "Object", + "tags": [], + "label": "coreUsageData", + "description": [], + "signature": [ + "{ incrementUsageCounter: ", + "CoreIncrementUsageCounter", + "; }" + ], + "path": "packages/core/status/core-status-server-internal/src/status_service.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], + "enums": [], + "misc": [ + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.StatusConfigType", + "type": "Type", + "tags": [], + "label": "StatusConfigType", + "description": [], + "signature": [ + "{ readonly allowAnonymous: boolean; }" + ], + "path": "packages/core/status/core-status-server-internal/src/status_config.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [ + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.statusConfig", + "type": "Object", + "tags": [], + "label": "statusConfig", + "description": [], + "path": "packages/core/status/core-status-server-internal/src/status_config.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.statusConfig.path", + "type": "string", + "tags": [], + "label": "path", + "description": [], + "path": "packages/core/status/core-status-server-internal/src/status_config.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-status-server-internal", + "id": "def-server.statusConfig.schema", + "type": "Object", + "tags": [], + "label": "schema", + "description": [], + "signature": [ + "ObjectType", + "<{ allowAnonymous: ", + "Type", + "; }>" + ], + "path": "packages/core/status/core-status-server-internal/src/status_config.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_core_status_server_internal.mdx b/api_docs/kbn_core_status_server_internal.mdx new file mode 100644 index 00000000000000..238921a2c6d7ad --- /dev/null +++ b/api_docs/kbn_core_status_server_internal.mdx @@ -0,0 +1,42 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnCoreStatusServerInternalPluginApi +slug: /kibana-dev-docs/api/kbn-core-status-server-internal +title: "@kbn/core-status-server-internal" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/core-status-server-internal plugin +date: 2022-09-14 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-internal'] +--- +import kbnCoreStatusServerInternalObj from './kbn_core_status_server_internal.devdocs.json'; + + + +Contact Kibana Core for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 22 | 0 | 22 | 1 | + +## Server + +### Objects + + +### Functions + + +### Classes + + +### Interfaces + + +### Consts, variables and types + + diff --git a/api_docs/kbn_core_status_server_mocks.devdocs.json b/api_docs/kbn_core_status_server_mocks.devdocs.json new file mode 100644 index 00000000000000..85f331d9e7e27c --- /dev/null +++ b/api_docs/kbn_core_status_server_mocks.devdocs.json @@ -0,0 +1,94 @@ +{ + "id": "@kbn/core-status-server-mocks", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [ + { + "parentPluginId": "@kbn/core-status-server-mocks", + "id": "def-server.statusServiceMock", + "type": "Object", + "tags": [], + "label": "statusServiceMock", + "description": [], + "path": "packages/core/status/core-status-server-mocks/src/status_service.mock.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-status-server-mocks", + "id": "def-server.statusServiceMock.create", + "type": "Function", + "tags": [], + "label": "create", + "description": [], + "signature": [ + "() => jest.Mocked" + ], + "path": "packages/core/status/core-status-server-mocks/src/status_service.mock.ts", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [] + }, + { + "parentPluginId": "@kbn/core-status-server-mocks", + "id": "def-server.statusServiceMock.createSetupContract", + "type": "Function", + "tags": [], + "label": "createSetupContract", + "description": [], + "signature": [ + "() => jest.Mocked<", + "StatusServiceSetup", + ">" + ], + "path": "packages/core/status/core-status-server-mocks/src/status_service.mock.ts", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [] + }, + { + "parentPluginId": "@kbn/core-status-server-mocks", + "id": "def-server.statusServiceMock.createInternalSetupContract", + "type": "Function", + "tags": [], + "label": "createInternalSetupContract", + "description": [], + "signature": [ + "() => jest.Mocked<", + "InternalStatusServiceSetup", + ">" + ], + "path": "packages/core/status/core-status-server-mocks/src/status_service.mock.ts", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [] + } + ], + "initialIsOpen": false + } + ] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_core_status_server_mocks.mdx b/api_docs/kbn_core_status_server_mocks.mdx new file mode 100644 index 00000000000000..87ac1a0cefc6b1 --- /dev/null +++ b/api_docs/kbn_core_status_server_mocks.mdx @@ -0,0 +1,30 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnCoreStatusServerMocksPluginApi +slug: /kibana-dev-docs/api/kbn-core-status-server-mocks +title: "@kbn/core-status-server-mocks" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/core-status-server-mocks plugin +date: 2022-09-14 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-mocks'] +--- +import kbnCoreStatusServerMocksObj from './kbn_core_status_server_mocks.devdocs.json'; + + + +Contact Kibana Core for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 4 | 0 | 4 | 0 | + +## Server + +### Objects + + diff --git a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx index bcc58ce97c1720..adb74f4d27fbc9 100644 --- a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx +++ b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-deprecations-getters title: "@kbn/core-test-helpers-deprecations-getters" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-deprecations-getters plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-deprecations-getters'] --- import kbnCoreTestHelpersDeprecationsGettersObj from './kbn_core_test_helpers_deprecations_getters.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_http_setup_browser.mdx b/api_docs/kbn_core_test_helpers_http_setup_browser.mdx index fab15b3ed6f35a..e8b26d5ebf2244 100644 --- a/api_docs/kbn_core_test_helpers_http_setup_browser.mdx +++ b/api_docs/kbn_core_test_helpers_http_setup_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-http-setup-browser title: "@kbn/core-test-helpers-http-setup-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-http-setup-browser plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-http-setup-browser'] --- import kbnCoreTestHelpersHttpSetupBrowserObj from './kbn_core_test_helpers_http_setup_browser.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser.mdx b/api_docs/kbn_core_theme_browser.mdx index 017403c279a718..e582cfe791e442 100644 --- a/api_docs/kbn_core_theme_browser.mdx +++ b/api_docs/kbn_core_theme_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser title: "@kbn/core-theme-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser'] --- import kbnCoreThemeBrowserObj from './kbn_core_theme_browser.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser_internal.mdx b/api_docs/kbn_core_theme_browser_internal.mdx index 4b18d3cb540b5c..0c0820077ce086 100644 --- a/api_docs/kbn_core_theme_browser_internal.mdx +++ b/api_docs/kbn_core_theme_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser-internal title: "@kbn/core-theme-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser-internal'] --- import kbnCoreThemeBrowserInternalObj from './kbn_core_theme_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser_mocks.mdx b/api_docs/kbn_core_theme_browser_mocks.mdx index d68eda65b74784..4935ac32ef0f15 100644 --- a/api_docs/kbn_core_theme_browser_mocks.mdx +++ b/api_docs/kbn_core_theme_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser-mocks title: "@kbn/core-theme-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser-mocks'] --- import kbnCoreThemeBrowserMocksObj from './kbn_core_theme_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser.mdx b/api_docs/kbn_core_ui_settings_browser.mdx index d43b64677389ad..c70c66ad3d6d6c 100644 --- a/api_docs/kbn_core_ui_settings_browser.mdx +++ b/api_docs/kbn_core_ui_settings_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser title: "@kbn/core-ui-settings-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser'] --- import kbnCoreUiSettingsBrowserObj from './kbn_core_ui_settings_browser.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser_internal.mdx b/api_docs/kbn_core_ui_settings_browser_internal.mdx index a2b638ea5c8f3b..a02c0314d4e94f 100644 --- a/api_docs/kbn_core_ui_settings_browser_internal.mdx +++ b/api_docs/kbn_core_ui_settings_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser-internal title: "@kbn/core-ui-settings-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser-internal'] --- import kbnCoreUiSettingsBrowserInternalObj from './kbn_core_ui_settings_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser_mocks.mdx b/api_docs/kbn_core_ui_settings_browser_mocks.mdx index 6a7ad1ab8e14d6..01b82612891bcd 100644 --- a/api_docs/kbn_core_ui_settings_browser_mocks.mdx +++ b/api_docs/kbn_core_ui_settings_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser-mocks title: "@kbn/core-ui-settings-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser-mocks'] --- import kbnCoreUiSettingsBrowserMocksObj from './kbn_core_ui_settings_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_common.mdx b/api_docs/kbn_core_ui_settings_common.mdx index 9b16f81a2be4a9..12fd46c7d0487c 100644 --- a/api_docs/kbn_core_ui_settings_common.mdx +++ b/api_docs/kbn_core_ui_settings_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-common title: "@kbn/core-ui-settings-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-common plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-common'] --- import kbnCoreUiSettingsCommonObj from './kbn_core_ui_settings_common.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server.mdx b/api_docs/kbn_core_usage_data_server.mdx index 135387c16286c8..93513eca040979 100644 --- a/api_docs/kbn_core_usage_data_server.mdx +++ b/api_docs/kbn_core_usage_data_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server title: "@kbn/core-usage-data-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server'] --- import kbnCoreUsageDataServerObj from './kbn_core_usage_data_server.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server_internal.mdx b/api_docs/kbn_core_usage_data_server_internal.mdx index cb5b74bd34c0bd..7c79d1ef3919ad 100644 --- a/api_docs/kbn_core_usage_data_server_internal.mdx +++ b/api_docs/kbn_core_usage_data_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server-internal title: "@kbn/core-usage-data-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server-internal plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server-internal'] --- import kbnCoreUsageDataServerInternalObj from './kbn_core_usage_data_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server_mocks.mdx b/api_docs/kbn_core_usage_data_server_mocks.mdx index 02f152900b4477..241103a0a2151f 100644 --- a/api_docs/kbn_core_usage_data_server_mocks.mdx +++ b/api_docs/kbn_core_usage_data_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server-mocks title: "@kbn/core-usage-data-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server-mocks'] --- import kbnCoreUsageDataServerMocksObj from './kbn_core_usage_data_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_crypto.mdx b/api_docs/kbn_crypto.mdx index 70e8d69a6b17cb..e2e3900bb3f10d 100644 --- a/api_docs/kbn_crypto.mdx +++ b/api_docs/kbn_crypto.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-crypto title: "@kbn/crypto" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/crypto plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto'] --- import kbnCryptoObj from './kbn_crypto.devdocs.json'; diff --git a/api_docs/kbn_crypto_browser.mdx b/api_docs/kbn_crypto_browser.mdx index 77788c27073dcd..c807c0dc574e8a 100644 --- a/api_docs/kbn_crypto_browser.mdx +++ b/api_docs/kbn_crypto_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-crypto-browser title: "@kbn/crypto-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/crypto-browser plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto-browser'] --- import kbnCryptoBrowserObj from './kbn_crypto_browser.devdocs.json'; diff --git a/api_docs/kbn_datemath.mdx b/api_docs/kbn_datemath.mdx index 669752ea179918..7d471d0ea41179 100644 --- a/api_docs/kbn_datemath.mdx +++ b/api_docs/kbn_datemath.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-datemath title: "@kbn/datemath" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/datemath plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/datemath'] --- import kbnDatemathObj from './kbn_datemath.devdocs.json'; diff --git a/api_docs/kbn_dev_cli_errors.mdx b/api_docs/kbn_dev_cli_errors.mdx index 653e1b5efe544b..597b6b0fb45856 100644 --- a/api_docs/kbn_dev_cli_errors.mdx +++ b/api_docs/kbn_dev_cli_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-errors title: "@kbn/dev-cli-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-cli-errors plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-errors'] --- import kbnDevCliErrorsObj from './kbn_dev_cli_errors.devdocs.json'; diff --git a/api_docs/kbn_dev_cli_runner.mdx b/api_docs/kbn_dev_cli_runner.mdx index f7edb115f49a9f..b71a8189686666 100644 --- a/api_docs/kbn_dev_cli_runner.mdx +++ b/api_docs/kbn_dev_cli_runner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-runner title: "@kbn/dev-cli-runner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-cli-runner plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-runner'] --- import kbnDevCliRunnerObj from './kbn_dev_cli_runner.devdocs.json'; diff --git a/api_docs/kbn_dev_proc_runner.mdx b/api_docs/kbn_dev_proc_runner.mdx index f83c97ef5f49d9..94b37a203404a5 100644 --- a/api_docs/kbn_dev_proc_runner.mdx +++ b/api_docs/kbn_dev_proc_runner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-proc-runner title: "@kbn/dev-proc-runner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-proc-runner plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-proc-runner'] --- import kbnDevProcRunnerObj from './kbn_dev_proc_runner.devdocs.json'; diff --git a/api_docs/kbn_dev_utils.mdx b/api_docs/kbn_dev_utils.mdx index 7278c02638113f..92771deb71a7ba 100644 --- a/api_docs/kbn_dev_utils.mdx +++ b/api_docs/kbn_dev_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-utils title: "@kbn/dev-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-utils plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-utils'] --- import kbnDevUtilsObj from './kbn_dev_utils.devdocs.json'; diff --git a/api_docs/kbn_doc_links.devdocs.json b/api_docs/kbn_doc_links.devdocs.json index 4dcf21464cd010..b8d975b8f58143 100644 --- a/api_docs/kbn_doc_links.devdocs.json +++ b/api_docs/kbn_doc_links.devdocs.json @@ -546,7 +546,7 @@ "label": "securitySolution", "description": [], "signature": [ - "{ readonly trustedApps: string; readonly eventFilters: string; readonly blocklist: string; readonly policyResponseTroubleshooting: { full_disk_access: string; macos_system_ext: string; linux_deadlock: string; }; readonly threatIntelInt: string; readonly responseActions: string; }" + "{ readonly trustedApps: string; readonly eventFilters: string; readonly blocklist: string; readonly policyResponseTroubleshooting: { full_disk_access: string; macos_system_ext: string; linux_deadlock: string; }; readonly packageActionTroubleshooting: { es_connection: string; }; readonly threatIntelInt: string; readonly responseActions: string; }" ], "path": "packages/kbn-doc-links/src/types.ts", "deprecated": false, diff --git a/api_docs/kbn_doc_links.mdx b/api_docs/kbn_doc_links.mdx index 044c765674ea12..e63e20a0dc14f1 100644 --- a/api_docs/kbn_doc_links.mdx +++ b/api_docs/kbn_doc_links.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-doc-links title: "@kbn/doc-links" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/doc-links plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/doc-links'] --- import kbnDocLinksObj from './kbn_doc_links.devdocs.json'; diff --git a/api_docs/kbn_docs_utils.mdx b/api_docs/kbn_docs_utils.mdx index d47c4a94ac5345..f255e24cd434b0 100644 --- a/api_docs/kbn_docs_utils.mdx +++ b/api_docs/kbn_docs_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-docs-utils title: "@kbn/docs-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/docs-utils plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/docs-utils'] --- import kbnDocsUtilsObj from './kbn_docs_utils.devdocs.json'; diff --git a/api_docs/kbn_ebt_tools.mdx b/api_docs/kbn_ebt_tools.mdx index 5cc4b3d097f4ff..133112f16c0af1 100644 --- a/api_docs/kbn_ebt_tools.mdx +++ b/api_docs/kbn_ebt_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ebt-tools title: "@kbn/ebt-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ebt-tools plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ebt-tools'] --- import kbnEbtToolsObj from './kbn_ebt_tools.devdocs.json'; diff --git a/api_docs/kbn_es_archiver.mdx b/api_docs/kbn_es_archiver.mdx index 5cad59a4b8f74d..3cf38907f8097f 100644 --- a/api_docs/kbn_es_archiver.mdx +++ b/api_docs/kbn_es_archiver.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-archiver title: "@kbn/es-archiver" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-archiver plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-archiver'] --- import kbnEsArchiverObj from './kbn_es_archiver.devdocs.json'; diff --git a/api_docs/kbn_es_errors.mdx b/api_docs/kbn_es_errors.mdx index ca0ada58480340..3775e5e6f429c7 100644 --- a/api_docs/kbn_es_errors.mdx +++ b/api_docs/kbn_es_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-errors title: "@kbn/es-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-errors plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-errors'] --- import kbnEsErrorsObj from './kbn_es_errors.devdocs.json'; diff --git a/api_docs/kbn_es_query.mdx b/api_docs/kbn_es_query.mdx index 792b606b4133a0..1d58fd3268ebec 100644 --- a/api_docs/kbn_es_query.mdx +++ b/api_docs/kbn_es_query.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-query title: "@kbn/es-query" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-query plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-query'] --- import kbnEsQueryObj from './kbn_es_query.devdocs.json'; diff --git a/api_docs/kbn_eslint_plugin_imports.mdx b/api_docs/kbn_eslint_plugin_imports.mdx index 6e1eb7f3fcfde7..ad0131d2c1f08a 100644 --- a/api_docs/kbn_eslint_plugin_imports.mdx +++ b/api_docs/kbn_eslint_plugin_imports.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-eslint-plugin-imports title: "@kbn/eslint-plugin-imports" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/eslint-plugin-imports plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/eslint-plugin-imports'] --- import kbnEslintPluginImportsObj from './kbn_eslint_plugin_imports.devdocs.json'; diff --git a/api_docs/kbn_field_types.mdx b/api_docs/kbn_field_types.mdx index 3fa74decb5caac..f553ab46433ae9 100644 --- a/api_docs/kbn_field_types.mdx +++ b/api_docs/kbn_field_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-field-types title: "@kbn/field-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/field-types plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/field-types'] --- import kbnFieldTypesObj from './kbn_field_types.devdocs.json'; diff --git a/api_docs/kbn_find_used_node_modules.mdx b/api_docs/kbn_find_used_node_modules.mdx index 5c3fcdaab984cb..9b389dd28589d3 100644 --- a/api_docs/kbn_find_used_node_modules.mdx +++ b/api_docs/kbn_find_used_node_modules.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-find-used-node-modules title: "@kbn/find-used-node-modules" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/find-used-node-modules plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/find-used-node-modules'] --- import kbnFindUsedNodeModulesObj from './kbn_find_used_node_modules.devdocs.json'; diff --git a/api_docs/kbn_generate.mdx b/api_docs/kbn_generate.mdx index ed9435a3b3b748..0e698a9929064e 100644 --- a/api_docs/kbn_generate.mdx +++ b/api_docs/kbn_generate.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate title: "@kbn/generate" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate'] --- import kbnGenerateObj from './kbn_generate.devdocs.json'; diff --git a/api_docs/kbn_get_repo_files.mdx b/api_docs/kbn_get_repo_files.mdx index 3e663271b7a7d4..0d106a0c1c3b77 100644 --- a/api_docs/kbn_get_repo_files.mdx +++ b/api_docs/kbn_get_repo_files.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-get-repo-files title: "@kbn/get-repo-files" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/get-repo-files plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/get-repo-files'] --- import kbnGetRepoFilesObj from './kbn_get_repo_files.devdocs.json'; diff --git a/api_docs/kbn_handlebars.mdx b/api_docs/kbn_handlebars.mdx index 89013d791f80d2..3c989779e0ca3f 100644 --- a/api_docs/kbn_handlebars.mdx +++ b/api_docs/kbn_handlebars.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-handlebars title: "@kbn/handlebars" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/handlebars plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/handlebars'] --- import kbnHandlebarsObj from './kbn_handlebars.devdocs.json'; diff --git a/api_docs/kbn_hapi_mocks.mdx b/api_docs/kbn_hapi_mocks.mdx index 03c70dae79e772..3ba78844ac8c61 100644 --- a/api_docs/kbn_hapi_mocks.mdx +++ b/api_docs/kbn_hapi_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-hapi-mocks title: "@kbn/hapi-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/hapi-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/hapi-mocks'] --- import kbnHapiMocksObj from './kbn_hapi_mocks.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_card.mdx b/api_docs/kbn_home_sample_data_card.mdx index 4f6ed6ee570a8f..e57c0bf243775c 100644 --- a/api_docs/kbn_home_sample_data_card.mdx +++ b/api_docs/kbn_home_sample_data_card.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-home-sample-data-card title: "@kbn/home-sample-data-card" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/home-sample-data-card plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/home-sample-data-card'] --- import kbnHomeSampleDataCardObj from './kbn_home_sample_data_card.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_tab.mdx b/api_docs/kbn_home_sample_data_tab.mdx index 0d51ff6ce14658..23c2c13ca7b380 100644 --- a/api_docs/kbn_home_sample_data_tab.mdx +++ b/api_docs/kbn_home_sample_data_tab.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-home-sample-data-tab title: "@kbn/home-sample-data-tab" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/home-sample-data-tab plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/home-sample-data-tab'] --- import kbnHomeSampleDataTabObj from './kbn_home_sample_data_tab.devdocs.json'; diff --git a/api_docs/kbn_i18n.mdx b/api_docs/kbn_i18n.mdx index dda867b946f49d..006b6904f15a91 100644 --- a/api_docs/kbn_i18n.mdx +++ b/api_docs/kbn_i18n.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-i18n title: "@kbn/i18n" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/i18n plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n'] --- import kbnI18nObj from './kbn_i18n.devdocs.json'; diff --git a/api_docs/kbn_import_resolver.mdx b/api_docs/kbn_import_resolver.mdx index 1eeda889c5cdd2..144b8c57d2399a 100644 --- a/api_docs/kbn_import_resolver.mdx +++ b/api_docs/kbn_import_resolver.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-import-resolver title: "@kbn/import-resolver" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/import-resolver plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/import-resolver'] --- import kbnImportResolverObj from './kbn_import_resolver.devdocs.json'; diff --git a/api_docs/kbn_interpreter.mdx b/api_docs/kbn_interpreter.mdx index 8c87bc7a914bd1..bccabbb56ce659 100644 --- a/api_docs/kbn_interpreter.mdx +++ b/api_docs/kbn_interpreter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-interpreter title: "@kbn/interpreter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/interpreter plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/interpreter'] --- import kbnInterpreterObj from './kbn_interpreter.devdocs.json'; diff --git a/api_docs/kbn_io_ts_utils.mdx b/api_docs/kbn_io_ts_utils.mdx index 1f89b3b7e535a0..a6f9d341f54716 100644 --- a/api_docs/kbn_io_ts_utils.mdx +++ b/api_docs/kbn_io_ts_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-io-ts-utils title: "@kbn/io-ts-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/io-ts-utils plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/io-ts-utils'] --- import kbnIoTsUtilsObj from './kbn_io_ts_utils.devdocs.json'; diff --git a/api_docs/kbn_jest_serializers.mdx b/api_docs/kbn_jest_serializers.mdx index 6885069422ac0a..241cec88b6b62b 100644 --- a/api_docs/kbn_jest_serializers.mdx +++ b/api_docs/kbn_jest_serializers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-jest-serializers title: "@kbn/jest-serializers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/jest-serializers plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/jest-serializers'] --- import kbnJestSerializersObj from './kbn_jest_serializers.devdocs.json'; diff --git a/api_docs/kbn_kibana_manifest_schema.mdx b/api_docs/kbn_kibana_manifest_schema.mdx index 771d6d1341fcaf..90976d0c3f5eab 100644 --- a/api_docs/kbn_kibana_manifest_schema.mdx +++ b/api_docs/kbn_kibana_manifest_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-kibana-manifest-schema title: "@kbn/kibana-manifest-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/kibana-manifest-schema plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/kibana-manifest-schema'] --- import kbnKibanaManifestSchemaObj from './kbn_kibana_manifest_schema.devdocs.json'; diff --git a/api_docs/kbn_logging.devdocs.json b/api_docs/kbn_logging.devdocs.json index 70e29f10ab2af0..ae5f4c09df969a 100644 --- a/api_docs/kbn_logging.devdocs.json +++ b/api_docs/kbn_logging.devdocs.json @@ -658,7 +658,7 @@ "label": "EcsEventKind", "description": [], "signature": [ - "\"metric\" | \"alert\" | \"state\" | \"event\" | \"signal\" | \"pipeline_error\"" + "\"metric\" | \"alert\" | \"signal\" | \"state\" | \"event\" | \"pipeline_error\"" ], "path": "packages/kbn-logging/src/ecs/event.ts", "deprecated": false, diff --git a/api_docs/kbn_logging.mdx b/api_docs/kbn_logging.mdx index eb7eb96ae58c22..730962402d1f56 100644 --- a/api_docs/kbn_logging.mdx +++ b/api_docs/kbn_logging.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-logging title: "@kbn/logging" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/logging plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging'] --- import kbnLoggingObj from './kbn_logging.devdocs.json'; diff --git a/api_docs/kbn_logging_mocks.mdx b/api_docs/kbn_logging_mocks.mdx index c778ebc86da322..c2ccc1133130b7 100644 --- a/api_docs/kbn_logging_mocks.mdx +++ b/api_docs/kbn_logging_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-logging-mocks title: "@kbn/logging-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/logging-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging-mocks'] --- import kbnLoggingMocksObj from './kbn_logging_mocks.devdocs.json'; diff --git a/api_docs/kbn_managed_vscode_config.mdx b/api_docs/kbn_managed_vscode_config.mdx index f9799e1ac39470..80c74ea2d234a0 100644 --- a/api_docs/kbn_managed_vscode_config.mdx +++ b/api_docs/kbn_managed_vscode_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-managed-vscode-config title: "@kbn/managed-vscode-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/managed-vscode-config plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/managed-vscode-config'] --- import kbnManagedVscodeConfigObj from './kbn_managed_vscode_config.devdocs.json'; diff --git a/api_docs/kbn_mapbox_gl.mdx b/api_docs/kbn_mapbox_gl.mdx index fc653bed6e7e95..3d5fe87b80b7ac 100644 --- a/api_docs/kbn_mapbox_gl.mdx +++ b/api_docs/kbn_mapbox_gl.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-mapbox-gl title: "@kbn/mapbox-gl" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/mapbox-gl plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/mapbox-gl'] --- import kbnMapboxGlObj from './kbn_mapbox_gl.devdocs.json'; diff --git a/api_docs/kbn_ml_agg_utils.mdx b/api_docs/kbn_ml_agg_utils.mdx index 12b0ecb60e66ca..b5ced4db599820 100644 --- a/api_docs/kbn_ml_agg_utils.mdx +++ b/api_docs/kbn_ml_agg_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-agg-utils title: "@kbn/ml-agg-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-agg-utils plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-agg-utils'] --- import kbnMlAggUtilsObj from './kbn_ml_agg_utils.devdocs.json'; @@ -21,7 +21,7 @@ Contact Machine Learning UI for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 53 | 2 | 35 | 4 | +| 53 | 2 | 35 | 3 | ## Server diff --git a/api_docs/kbn_ml_is_populated_object.mdx b/api_docs/kbn_ml_is_populated_object.mdx index 67f7ef712b4190..ffb5d3a34c4932 100644 --- a/api_docs/kbn_ml_is_populated_object.mdx +++ b/api_docs/kbn_ml_is_populated_object.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-is-populated-object title: "@kbn/ml-is-populated-object" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-is-populated-object plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-is-populated-object'] --- import kbnMlIsPopulatedObjectObj from './kbn_ml_is_populated_object.devdocs.json'; diff --git a/api_docs/kbn_ml_string_hash.mdx b/api_docs/kbn_ml_string_hash.mdx index a8aa07a6cb0ad5..a0a000693f3b18 100644 --- a/api_docs/kbn_ml_string_hash.mdx +++ b/api_docs/kbn_ml_string_hash.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-string-hash title: "@kbn/ml-string-hash" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-string-hash plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-string-hash'] --- import kbnMlStringHashObj from './kbn_ml_string_hash.devdocs.json'; diff --git a/api_docs/kbn_monaco.mdx b/api_docs/kbn_monaco.mdx index cf5344b7847eba..794db6515de609 100644 --- a/api_docs/kbn_monaco.mdx +++ b/api_docs/kbn_monaco.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-monaco title: "@kbn/monaco" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/monaco plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/monaco'] --- import kbnMonacoObj from './kbn_monaco.devdocs.json'; diff --git a/api_docs/kbn_optimizer.mdx b/api_docs/kbn_optimizer.mdx index 59384f2ce9406d..f033e284d5137d 100644 --- a/api_docs/kbn_optimizer.mdx +++ b/api_docs/kbn_optimizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer title: "@kbn/optimizer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/optimizer plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer'] --- import kbnOptimizerObj from './kbn_optimizer.devdocs.json'; diff --git a/api_docs/kbn_optimizer_webpack_helpers.mdx b/api_docs/kbn_optimizer_webpack_helpers.mdx index cb5c6845f2d4f4..6ce411e98566fe 100644 --- a/api_docs/kbn_optimizer_webpack_helpers.mdx +++ b/api_docs/kbn_optimizer_webpack_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer-webpack-helpers title: "@kbn/optimizer-webpack-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/optimizer-webpack-helpers plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer-webpack-helpers'] --- import kbnOptimizerWebpackHelpersObj from './kbn_optimizer_webpack_helpers.devdocs.json'; diff --git a/api_docs/kbn_performance_testing_dataset_extractor.mdx b/api_docs/kbn_performance_testing_dataset_extractor.mdx index 2445802229bf1d..e575352d1e1840 100644 --- a/api_docs/kbn_performance_testing_dataset_extractor.mdx +++ b/api_docs/kbn_performance_testing_dataset_extractor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-performance-testing-dataset-extractor title: "@kbn/performance-testing-dataset-extractor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/performance-testing-dataset-extractor plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/performance-testing-dataset-extractor'] --- import kbnPerformanceTestingDatasetExtractorObj from './kbn_performance_testing_dataset_extractor.devdocs.json'; diff --git a/api_docs/kbn_plugin_generator.mdx b/api_docs/kbn_plugin_generator.mdx index 617d5fec736bf8..ed3a60fb9e1176 100644 --- a/api_docs/kbn_plugin_generator.mdx +++ b/api_docs/kbn_plugin_generator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-generator title: "@kbn/plugin-generator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-generator plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-generator'] --- import kbnPluginGeneratorObj from './kbn_plugin_generator.devdocs.json'; diff --git a/api_docs/kbn_plugin_helpers.mdx b/api_docs/kbn_plugin_helpers.mdx index 44df77707e11de..f456dd6779be6b 100644 --- a/api_docs/kbn_plugin_helpers.mdx +++ b/api_docs/kbn_plugin_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-helpers title: "@kbn/plugin-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-helpers plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-helpers'] --- import kbnPluginHelpersObj from './kbn_plugin_helpers.devdocs.json'; diff --git a/api_docs/kbn_react_field.mdx b/api_docs/kbn_react_field.mdx index 6917f3fc99b3ba..685de62ad29bdf 100644 --- a/api_docs/kbn_react_field.mdx +++ b/api_docs/kbn_react_field.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-field title: "@kbn/react-field" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-field plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-field'] --- import kbnReactFieldObj from './kbn_react_field.devdocs.json'; diff --git a/api_docs/kbn_repo_source_classifier.mdx b/api_docs/kbn_repo_source_classifier.mdx index 9b30733e7280b3..863ba0bdf49b36 100644 --- a/api_docs/kbn_repo_source_classifier.mdx +++ b/api_docs/kbn_repo_source_classifier.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-source-classifier title: "@kbn/repo-source-classifier" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-source-classifier plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-source-classifier'] --- import kbnRepoSourceClassifierObj from './kbn_repo_source_classifier.devdocs.json'; diff --git a/api_docs/kbn_rule_data_utils.mdx b/api_docs/kbn_rule_data_utils.mdx index d278bd47a83cc6..4fee60c5a0cd3b 100644 --- a/api_docs/kbn_rule_data_utils.mdx +++ b/api_docs/kbn_rule_data_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rule-data-utils title: "@kbn/rule-data-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rule-data-utils plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rule-data-utils'] --- import kbnRuleDataUtilsObj from './kbn_rule_data_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_autocomplete.mdx b/api_docs/kbn_securitysolution_autocomplete.mdx index e1c8c47ea5d192..20d8b73b98a8ef 100644 --- a/api_docs/kbn_securitysolution_autocomplete.mdx +++ b/api_docs/kbn_securitysolution_autocomplete.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-autocomplete title: "@kbn/securitysolution-autocomplete" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-autocomplete plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-autocomplete'] --- import kbnSecuritysolutionAutocompleteObj from './kbn_securitysolution_autocomplete.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_es_utils.mdx b/api_docs/kbn_securitysolution_es_utils.mdx index d2a1be19c6c177..090573fa18a4ea 100644 --- a/api_docs/kbn_securitysolution_es_utils.mdx +++ b/api_docs/kbn_securitysolution_es_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-es-utils title: "@kbn/securitysolution-es-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-es-utils plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-es-utils'] --- import kbnSecuritysolutionEsUtilsObj from './kbn_securitysolution_es_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_hook_utils.mdx b/api_docs/kbn_securitysolution_hook_utils.mdx index e4fbf89a1405f1..951727fd01bab4 100644 --- a/api_docs/kbn_securitysolution_hook_utils.mdx +++ b/api_docs/kbn_securitysolution_hook_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-hook-utils title: "@kbn/securitysolution-hook-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-hook-utils plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-hook-utils'] --- import kbnSecuritysolutionHookUtilsObj from './kbn_securitysolution_hook_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx index 38f823267cd07d..3380e7363656be 100644 --- a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-alerting-types title: "@kbn/securitysolution-io-ts-alerting-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-alerting-types plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-alerting-types'] --- import kbnSecuritysolutionIoTsAlertingTypesObj from './kbn_securitysolution_io_ts_alerting_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_list_types.mdx b/api_docs/kbn_securitysolution_io_ts_list_types.mdx index adec0fb8b5efb8..bc11f64cb82ab1 100644 --- a/api_docs/kbn_securitysolution_io_ts_list_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_list_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-list-types title: "@kbn/securitysolution-io-ts-list-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-list-types plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-list-types'] --- import kbnSecuritysolutionIoTsListTypesObj from './kbn_securitysolution_io_ts_list_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_types.mdx b/api_docs/kbn_securitysolution_io_ts_types.mdx index cf020e8b0a27ae..4377dcc63b14fd 100644 --- a/api_docs/kbn_securitysolution_io_ts_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-types title: "@kbn/securitysolution-io-ts-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-types plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-types'] --- import kbnSecuritysolutionIoTsTypesObj from './kbn_securitysolution_io_ts_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_utils.mdx b/api_docs/kbn_securitysolution_io_ts_utils.mdx index 598a866af57e8a..dcc0837fbe70ab 100644 --- a/api_docs/kbn_securitysolution_io_ts_utils.mdx +++ b/api_docs/kbn_securitysolution_io_ts_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-utils title: "@kbn/securitysolution-io-ts-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-utils plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-utils'] --- import kbnSecuritysolutionIoTsUtilsObj from './kbn_securitysolution_io_ts_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_api.mdx b/api_docs/kbn_securitysolution_list_api.mdx index 7f8c8c61107518..c08c1eb11886a8 100644 --- a/api_docs/kbn_securitysolution_list_api.mdx +++ b/api_docs/kbn_securitysolution_list_api.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-api title: "@kbn/securitysolution-list-api" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-api plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-api'] --- import kbnSecuritysolutionListApiObj from './kbn_securitysolution_list_api.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_constants.mdx b/api_docs/kbn_securitysolution_list_constants.mdx index 3690bc8cbc2b4c..5dea66bcc50ca8 100644 --- a/api_docs/kbn_securitysolution_list_constants.mdx +++ b/api_docs/kbn_securitysolution_list_constants.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-constants title: "@kbn/securitysolution-list-constants" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-constants plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-constants'] --- import kbnSecuritysolutionListConstantsObj from './kbn_securitysolution_list_constants.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_hooks.mdx b/api_docs/kbn_securitysolution_list_hooks.mdx index e30499cbbf39a5..a91c003fa846cf 100644 --- a/api_docs/kbn_securitysolution_list_hooks.mdx +++ b/api_docs/kbn_securitysolution_list_hooks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-hooks title: "@kbn/securitysolution-list-hooks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-hooks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-hooks'] --- import kbnSecuritysolutionListHooksObj from './kbn_securitysolution_list_hooks.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_utils.mdx b/api_docs/kbn_securitysolution_list_utils.mdx index 22657a04b1bb90..ab11c5ec014a39 100644 --- a/api_docs/kbn_securitysolution_list_utils.mdx +++ b/api_docs/kbn_securitysolution_list_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-utils title: "@kbn/securitysolution-list-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-utils plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-utils'] --- import kbnSecuritysolutionListUtilsObj from './kbn_securitysolution_list_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_rules.mdx b/api_docs/kbn_securitysolution_rules.mdx index 19a2c081a5bd76..64693afed8e1e7 100644 --- a/api_docs/kbn_securitysolution_rules.mdx +++ b/api_docs/kbn_securitysolution_rules.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-rules title: "@kbn/securitysolution-rules" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-rules plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-rules'] --- import kbnSecuritysolutionRulesObj from './kbn_securitysolution_rules.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_t_grid.mdx b/api_docs/kbn_securitysolution_t_grid.mdx index 65f02eb6305e73..93f2ecdf089ee9 100644 --- a/api_docs/kbn_securitysolution_t_grid.mdx +++ b/api_docs/kbn_securitysolution_t_grid.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-t-grid title: "@kbn/securitysolution-t-grid" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-t-grid plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-t-grid'] --- import kbnSecuritysolutionTGridObj from './kbn_securitysolution_t_grid.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_utils.mdx b/api_docs/kbn_securitysolution_utils.mdx index f84326704b432b..3f4e77e20a1419 100644 --- a/api_docs/kbn_securitysolution_utils.mdx +++ b/api_docs/kbn_securitysolution_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-utils title: "@kbn/securitysolution-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-utils plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-utils'] --- import kbnSecuritysolutionUtilsObj from './kbn_securitysolution_utils.devdocs.json'; diff --git a/api_docs/kbn_server_http_tools.mdx b/api_docs/kbn_server_http_tools.mdx index 2553be04e72c4f..ffe048a24fb69c 100644 --- a/api_docs/kbn_server_http_tools.mdx +++ b/api_docs/kbn_server_http_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-http-tools title: "@kbn/server-http-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-http-tools plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-http-tools'] --- import kbnServerHttpToolsObj from './kbn_server_http_tools.devdocs.json'; diff --git a/api_docs/kbn_server_route_repository.mdx b/api_docs/kbn_server_route_repository.mdx index e1deb3f5d47f77..37efe76fa4ff19 100644 --- a/api_docs/kbn_server_route_repository.mdx +++ b/api_docs/kbn_server_route_repository.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-route-repository title: "@kbn/server-route-repository" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-route-repository plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-route-repository'] --- import kbnServerRouteRepositoryObj from './kbn_server_route_repository.devdocs.json'; diff --git a/api_docs/kbn_shared_svg.mdx b/api_docs/kbn_shared_svg.mdx index e28e8dfe7c35ae..32ac4de1779b95 100644 --- a/api_docs/kbn_shared_svg.mdx +++ b/api_docs/kbn_shared_svg.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-svg title: "@kbn/shared-svg" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-svg plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-svg'] --- import kbnSharedSvgObj from './kbn_shared_svg.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx b/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx index 57ee3c5eaeeac4..a96929ff84e0f5 100644 --- a/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx +++ b/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-exit-full-screen-mocks title: "@kbn/shared-ux-button-exit-full-screen-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-exit-full-screen-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-exit-full-screen-mocks'] --- import kbnSharedUxButtonExitFullScreenMocksObj from './kbn_shared_ux_button_exit_full_screen_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_toolbar.mdx b/api_docs/kbn_shared_ux_button_toolbar.mdx index 823e9843677a0e..0ceddd48be4803 100644 --- a/api_docs/kbn_shared_ux_button_toolbar.mdx +++ b/api_docs/kbn_shared_ux_button_toolbar.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-toolbar title: "@kbn/shared-ux-button-toolbar" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-toolbar plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-toolbar'] --- import kbnSharedUxButtonToolbarObj from './kbn_shared_ux_button_toolbar.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_card_no_data.mdx b/api_docs/kbn_shared_ux_card_no_data.mdx index 27455c0dcff207..b65b34b6610514 100644 --- a/api_docs/kbn_shared_ux_card_no_data.mdx +++ b/api_docs/kbn_shared_ux_card_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data title: "@kbn/shared-ux-card-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-card-no-data plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data'] --- import kbnSharedUxCardNoDataObj from './kbn_shared_ux_card_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_card_no_data_mocks.mdx b/api_docs/kbn_shared_ux_card_no_data_mocks.mdx index 30e9422c7d0dcc..cac824df68e3bd 100644 --- a/api_docs/kbn_shared_ux_card_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_card_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data-mocks title: "@kbn/shared-ux-card-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-card-no-data-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data-mocks'] --- import kbnSharedUxCardNoDataMocksObj from './kbn_shared_ux_card_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx b/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx index d382db6b061811..c849467b87c390 100644 --- a/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx +++ b/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-link-redirect-app-mocks title: "@kbn/shared-ux-link-redirect-app-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-link-redirect-app-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-link-redirect-app-mocks'] --- import kbnSharedUxLinkRedirectAppMocksObj from './kbn_shared_ux_link_redirect_app_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx index f719f64919ffe1..c22dc6579bbbe4 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data title: "@kbn/shared-ux-page-analytics-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-analytics-no-data plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data'] --- import kbnSharedUxPageAnalyticsNoDataObj from './kbn_shared_ux_page_analytics_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx index 6c98183d2a7ff0..cffe0245418138 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data-mocks title: "@kbn/shared-ux-page-analytics-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-analytics-no-data-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data-mocks'] --- import kbnSharedUxPageAnalyticsNoDataMocksObj from './kbn_shared_ux_page_analytics_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx index e96fd6b65b797d..bfe07bebc8867b 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data title: "@kbn/shared-ux-page-kibana-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-no-data plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data'] --- import kbnSharedUxPageKibanaNoDataObj from './kbn_shared_ux_page_kibana_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx index f082939bf23db8..a343b907a7d081 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data-mocks title: "@kbn/shared-ux-page-kibana-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-no-data-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data-mocks'] --- import kbnSharedUxPageKibanaNoDataMocksObj from './kbn_shared_ux_page_kibana_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_template.mdx b/api_docs/kbn_shared_ux_page_kibana_template.mdx index 56d513e6b23953..a16d453687be76 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_template.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-template title: "@kbn/shared-ux-page-kibana-template" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-template plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-template'] --- import kbnSharedUxPageKibanaTemplateObj from './kbn_shared_ux_page_kibana_template.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx b/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx index f2aa470f834cc5..d4ede6a1ac41b0 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-template-mocks title: "@kbn/shared-ux-page-kibana-template-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-template-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-template-mocks'] --- import kbnSharedUxPageKibanaTemplateMocksObj from './kbn_shared_ux_page_kibana_template_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data.mdx b/api_docs/kbn_shared_ux_page_no_data.mdx index 003b096cfd0f91..4f0577c8b5cb2e 100644 --- a/api_docs/kbn_shared_ux_page_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data title: "@kbn/shared-ux-page-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data'] --- import kbnSharedUxPageNoDataObj from './kbn_shared_ux_page_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_config.mdx b/api_docs/kbn_shared_ux_page_no_data_config.mdx index 8b61bc3b8fb032..3abcb1bbbdd0bc 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-config title: "@kbn/shared-ux-page-no-data-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-config plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-config'] --- import kbnSharedUxPageNoDataConfigObj from './kbn_shared_ux_page_no_data_config.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx b/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx index 9b0e41366e4922..5dd3cb2c786d66 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-config-mocks title: "@kbn/shared-ux-page-no-data-config-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-config-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-config-mocks'] --- import kbnSharedUxPageNoDataConfigMocksObj from './kbn_shared_ux_page_no_data_config_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_no_data_mocks.mdx index 673f7aa80d4184..faa271e5be519f 100644 --- a/api_docs/kbn_shared_ux_page_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-mocks title: "@kbn/shared-ux-page-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-mocks'] --- import kbnSharedUxPageNoDataMocksObj from './kbn_shared_ux_page_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_solution_nav.mdx b/api_docs/kbn_shared_ux_page_solution_nav.mdx index fca609edd1eec1..61ad1685894323 100644 --- a/api_docs/kbn_shared_ux_page_solution_nav.mdx +++ b/api_docs/kbn_shared_ux_page_solution_nav.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-solution-nav title: "@kbn/shared-ux-page-solution-nav" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-solution-nav plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-solution-nav'] --- import kbnSharedUxPageSolutionNavObj from './kbn_shared_ux_page_solution_nav.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx index d55f975104ffac..c40132ef203036 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views title: "@kbn/shared-ux-prompt-no-data-views" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-no-data-views plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views'] --- import kbnSharedUxPromptNoDataViewsObj from './kbn_shared_ux_prompt_no_data_views.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx index 370bb3fb7b081a..59dc8e8d829e0d 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views-mocks title: "@kbn/shared-ux-prompt-no-data-views-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-no-data-views-mocks plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views-mocks'] --- import kbnSharedUxPromptNoDataViewsMocksObj from './kbn_shared_ux_prompt_no_data_views_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router.devdocs.json b/api_docs/kbn_shared_ux_router.devdocs.json new file mode 100644 index 00000000000000..b3381733e9f8f1 --- /dev/null +++ b/api_docs/kbn_shared_ux_router.devdocs.json @@ -0,0 +1,65 @@ +{ + "id": "@kbn/shared-ux-router", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [ + { + "parentPluginId": "@kbn/shared-ux-router", + "id": "def-common.Route", + "type": "Function", + "tags": [], + "label": "Route", + "description": [ + "\nThis is a wrapper around the react-router-dom Route component that inserts\nMatchPropagator in every application route. It helps track all route changes\nand send them to the execution context, later used to enrich APM\n'route-change' transactions." + ], + "signature": [ + "({ children, component: Component, render, ...rest }: ", + "RouteProps", + ") => JSX.Element" + ], + "path": "packages/shared-ux/router/impl/router.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/shared-ux-router", + "id": "def-common.Route.$1", + "type": "Object", + "tags": [], + "label": "{ children, component: Component, render, ...rest }", + "description": [], + "signature": [ + "RouteProps" + ], + "path": "packages/shared-ux/router/impl/router.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_shared_ux_router.mdx b/api_docs/kbn_shared_ux_router.mdx new file mode 100644 index 00000000000000..4bca66716fc9d5 --- /dev/null +++ b/api_docs/kbn_shared_ux_router.mdx @@ -0,0 +1,30 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnSharedUxRouterPluginApi +slug: /kibana-dev-docs/api/kbn-shared-ux-router +title: "@kbn/shared-ux-router" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/shared-ux-router plugin +date: 2022-09-14 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router'] +--- +import kbnSharedUxRouterObj from './kbn_shared_ux_router.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 2 | 0 | 1 | 0 | + +## Common + +### Functions + + diff --git a/api_docs/kbn_shared_ux_router_mocks.devdocs.json b/api_docs/kbn_shared_ux_router_mocks.devdocs.json new file mode 100644 index 00000000000000..db66a624416979 --- /dev/null +++ b/api_docs/kbn_shared_ux_router_mocks.devdocs.json @@ -0,0 +1,45 @@ +{ + "id": "@kbn/shared-ux-router-mocks", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [ + { + "parentPluginId": "@kbn/shared-ux-router-mocks", + "id": "def-common.foo", + "type": "Function", + "tags": [], + "label": "foo", + "description": [], + "signature": [ + "() => string" + ], + "path": "packages/shared-ux/router/mocks/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_shared_ux_router_mocks.mdx b/api_docs/kbn_shared_ux_router_mocks.mdx new file mode 100644 index 00000000000000..f2b1cd67a2944d --- /dev/null +++ b/api_docs/kbn_shared_ux_router_mocks.mdx @@ -0,0 +1,30 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnSharedUxRouterMocksPluginApi +slug: /kibana-dev-docs/api/kbn-shared-ux-router-mocks +title: "@kbn/shared-ux-router-mocks" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/shared-ux-router-mocks plugin +date: 2022-09-14 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router-mocks'] +--- +import kbnSharedUxRouterMocksObj from './kbn_shared_ux_router_mocks.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 1 | 0 | 1 | 0 | + +## Common + +### Functions + + diff --git a/api_docs/kbn_shared_ux_storybook_config.mdx b/api_docs/kbn_shared_ux_storybook_config.mdx index 9200b11a09b215..02da141518fe96 100644 --- a/api_docs/kbn_shared_ux_storybook_config.mdx +++ b/api_docs/kbn_shared_ux_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook-config title: "@kbn/shared-ux-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-storybook-config plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook-config'] --- import kbnSharedUxStorybookConfigObj from './kbn_shared_ux_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_storybook_mock.mdx b/api_docs/kbn_shared_ux_storybook_mock.mdx index d281f952bbbdfd..3e856b3a242a05 100644 --- a/api_docs/kbn_shared_ux_storybook_mock.mdx +++ b/api_docs/kbn_shared_ux_storybook_mock.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook-mock title: "@kbn/shared-ux-storybook-mock" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-storybook-mock plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook-mock'] --- import kbnSharedUxStorybookMockObj from './kbn_shared_ux_storybook_mock.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_utility.mdx b/api_docs/kbn_shared_ux_utility.mdx index ce6cfca9bcf5c8..87a43ba20f6564 100644 --- a/api_docs/kbn_shared_ux_utility.mdx +++ b/api_docs/kbn_shared_ux_utility.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-utility title: "@kbn/shared-ux-utility" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-utility plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-utility'] --- import kbnSharedUxUtilityObj from './kbn_shared_ux_utility.devdocs.json'; diff --git a/api_docs/kbn_some_dev_log.mdx b/api_docs/kbn_some_dev_log.mdx index ef9f4272e5ae16..5aecc33c868ed1 100644 --- a/api_docs/kbn_some_dev_log.mdx +++ b/api_docs/kbn_some_dev_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-some-dev-log title: "@kbn/some-dev-log" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/some-dev-log plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/some-dev-log'] --- import kbnSomeDevLogObj from './kbn_some_dev_log.devdocs.json'; diff --git a/api_docs/kbn_sort_package_json.mdx b/api_docs/kbn_sort_package_json.mdx index 5e5cd15a358740..e31bf28072d6d9 100644 --- a/api_docs/kbn_sort_package_json.mdx +++ b/api_docs/kbn_sort_package_json.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-sort-package-json title: "@kbn/sort-package-json" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/sort-package-json plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/sort-package-json'] --- import kbnSortPackageJsonObj from './kbn_sort_package_json.devdocs.json'; diff --git a/api_docs/kbn_std.mdx b/api_docs/kbn_std.mdx index f09537bfe88aef..917e18a2e285c9 100644 --- a/api_docs/kbn_std.mdx +++ b/api_docs/kbn_std.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-std title: "@kbn/std" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/std plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/std'] --- import kbnStdObj from './kbn_std.devdocs.json'; diff --git a/api_docs/kbn_stdio_dev_helpers.mdx b/api_docs/kbn_stdio_dev_helpers.mdx index 9bd71879017ce6..fb9a9211338b65 100644 --- a/api_docs/kbn_stdio_dev_helpers.mdx +++ b/api_docs/kbn_stdio_dev_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-stdio-dev-helpers title: "@kbn/stdio-dev-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/stdio-dev-helpers plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/stdio-dev-helpers'] --- import kbnStdioDevHelpersObj from './kbn_stdio_dev_helpers.devdocs.json'; diff --git a/api_docs/kbn_storybook.mdx b/api_docs/kbn_storybook.mdx index 9a002696753d01..8980129eb4a833 100644 --- a/api_docs/kbn_storybook.mdx +++ b/api_docs/kbn_storybook.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-storybook title: "@kbn/storybook" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/storybook plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/storybook'] --- import kbnStorybookObj from './kbn_storybook.devdocs.json'; diff --git a/api_docs/kbn_telemetry_tools.mdx b/api_docs/kbn_telemetry_tools.mdx index 5b0eba5853c138..217ffd8731a136 100644 --- a/api_docs/kbn_telemetry_tools.mdx +++ b/api_docs/kbn_telemetry_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-telemetry-tools title: "@kbn/telemetry-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/telemetry-tools plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/telemetry-tools'] --- import kbnTelemetryToolsObj from './kbn_telemetry_tools.devdocs.json'; diff --git a/api_docs/kbn_test.devdocs.json b/api_docs/kbn_test.devdocs.json index 3a4cf723d0775e..f36daceb7cab27 100644 --- a/api_docs/kbn_test.devdocs.json +++ b/api_docs/kbn_test.devdocs.json @@ -2669,6 +2669,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "@kbn/test", + "id": "def-server.CreateTestEsClusterOptions.writeLogsToPath", + "type": "string", + "tags": [], + "label": "writeLogsToPath", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-test/src/es/test_es_cluster.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "@kbn/test", "id": "def-server.CreateTestEsClusterOptions.nodes", diff --git a/api_docs/kbn_test.mdx b/api_docs/kbn_test.mdx index 2b1cfb4d5c2fe0..efaa96574b84da 100644 --- a/api_docs/kbn_test.mdx +++ b/api_docs/kbn_test.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test title: "@kbn/test" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test'] --- import kbnTestObj from './kbn_test.devdocs.json'; @@ -21,7 +21,7 @@ Contact Operations for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 253 | 5 | 212 | 11 | +| 254 | 5 | 213 | 11 | ## Server diff --git a/api_docs/kbn_test_jest_helpers.mdx b/api_docs/kbn_test_jest_helpers.mdx index 20c4a3eb4f9ba7..dd6042710cbbd1 100644 --- a/api_docs/kbn_test_jest_helpers.mdx +++ b/api_docs/kbn_test_jest_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-jest-helpers title: "@kbn/test-jest-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test-jest-helpers plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-jest-helpers'] --- import kbnTestJestHelpersObj from './kbn_test_jest_helpers.devdocs.json'; diff --git a/api_docs/kbn_tooling_log.mdx b/api_docs/kbn_tooling_log.mdx index fb392e818e687e..24835b96e8ce50 100644 --- a/api_docs/kbn_tooling_log.mdx +++ b/api_docs/kbn_tooling_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-tooling-log title: "@kbn/tooling-log" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/tooling-log plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/tooling-log'] --- import kbnToolingLogObj from './kbn_tooling_log.devdocs.json'; diff --git a/api_docs/kbn_type_summarizer.mdx b/api_docs/kbn_type_summarizer.mdx index c06226b8eba969..4ffefbee6021a9 100644 --- a/api_docs/kbn_type_summarizer.mdx +++ b/api_docs/kbn_type_summarizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-type-summarizer title: "@kbn/type-summarizer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/type-summarizer plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/type-summarizer'] --- import kbnTypeSummarizerObj from './kbn_type_summarizer.devdocs.json'; diff --git a/api_docs/kbn_type_summarizer_core.mdx b/api_docs/kbn_type_summarizer_core.mdx index b81adb3a56d1d1..9b5b53144ed8cc 100644 --- a/api_docs/kbn_type_summarizer_core.mdx +++ b/api_docs/kbn_type_summarizer_core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-type-summarizer-core title: "@kbn/type-summarizer-core" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/type-summarizer-core plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/type-summarizer-core'] --- import kbnTypeSummarizerCoreObj from './kbn_type_summarizer_core.devdocs.json'; diff --git a/api_docs/kbn_typed_react_router_config.mdx b/api_docs/kbn_typed_react_router_config.mdx index e75d872f2484db..b59952bf2ea5f7 100644 --- a/api_docs/kbn_typed_react_router_config.mdx +++ b/api_docs/kbn_typed_react_router_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-typed-react-router-config title: "@kbn/typed-react-router-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/typed-react-router-config plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/typed-react-router-config'] --- import kbnTypedReactRouterConfigObj from './kbn_typed_react_router_config.devdocs.json'; diff --git a/api_docs/kbn_ui_theme.mdx b/api_docs/kbn_ui_theme.mdx index b70ee2231fefb1..2b66c71610519e 100644 --- a/api_docs/kbn_ui_theme.mdx +++ b/api_docs/kbn_ui_theme.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-theme title: "@kbn/ui-theme" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-theme plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-theme'] --- import kbnUiThemeObj from './kbn_ui_theme.devdocs.json'; diff --git a/api_docs/kbn_user_profile_components.mdx b/api_docs/kbn_user_profile_components.mdx index b7ddf660ca0df5..f782a75c0f46f0 100644 --- a/api_docs/kbn_user_profile_components.mdx +++ b/api_docs/kbn_user_profile_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-user-profile-components title: "@kbn/user-profile-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/user-profile-components plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/user-profile-components'] --- import kbnUserProfileComponentsObj from './kbn_user_profile_components.devdocs.json'; diff --git a/api_docs/kbn_utility_types.mdx b/api_docs/kbn_utility_types.mdx index 92cd97760ca7c9..65a98cddda8a23 100644 --- a/api_docs/kbn_utility_types.mdx +++ b/api_docs/kbn_utility_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types title: "@kbn/utility-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utility-types plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types'] --- import kbnUtilityTypesObj from './kbn_utility_types.devdocs.json'; diff --git a/api_docs/kbn_utility_types_jest.mdx b/api_docs/kbn_utility_types_jest.mdx index af84fa702b0759..0b59c7f16b477f 100644 --- a/api_docs/kbn_utility_types_jest.mdx +++ b/api_docs/kbn_utility_types_jest.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types-jest title: "@kbn/utility-types-jest" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utility-types-jest plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types-jest'] --- import kbnUtilityTypesJestObj from './kbn_utility_types_jest.devdocs.json'; diff --git a/api_docs/kbn_utils.mdx b/api_docs/kbn_utils.mdx index c6f2cc8482954a..252afe65df9a45 100644 --- a/api_docs/kbn_utils.mdx +++ b/api_docs/kbn_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utils title: "@kbn/utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utils plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utils'] --- import kbnUtilsObj from './kbn_utils.devdocs.json'; diff --git a/api_docs/kbn_yarn_lock_validator.mdx b/api_docs/kbn_yarn_lock_validator.mdx index 789133a70ac7ec..4dbfb88ad580f3 100644 --- a/api_docs/kbn_yarn_lock_validator.mdx +++ b/api_docs/kbn_yarn_lock_validator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-yarn-lock-validator title: "@kbn/yarn-lock-validator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/yarn-lock-validator plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/yarn-lock-validator'] --- import kbnYarnLockValidatorObj from './kbn_yarn_lock_validator.devdocs.json'; diff --git a/api_docs/kibana_overview.mdx b/api_docs/kibana_overview.mdx index 4ea296d9ab1e1a..743067ff0b3b51 100644 --- a/api_docs/kibana_overview.mdx +++ b/api_docs/kibana_overview.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaOverview title: "kibanaOverview" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaOverview plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaOverview'] --- import kibanaOverviewObj from './kibana_overview.devdocs.json'; diff --git a/api_docs/kibana_react.devdocs.json b/api_docs/kibana_react.devdocs.json index d125ca647c266d..61f1d1a3e264d0 100644 --- a/api_docs/kibana_react.devdocs.json +++ b/api_docs/kibana_react.devdocs.json @@ -211,7 +211,8 @@ "label": "debouncedFetch", "description": [], "signature": [ - "((filter: string) => Promise) & _.Cancelable" + "((filter: string) => Promise) & ", + "Cancelable" ], "path": "src/plugins/kibana_react/public/table_list_view/table_list_view.tsx", "deprecated": false, @@ -1703,18 +1704,6 @@ "deprecated": true, "trackAdoption": false, "references": [ - { - "plugin": "esUiShared", - "path": "src/plugins/es_ui_shared/public/components/view_api_request_flyout/view_api_request_flyout.tsx" - }, - { - "plugin": "esUiShared", - "path": "src/plugins/es_ui_shared/public/components/view_api_request_flyout/view_api_request_flyout.tsx" - }, - { - "plugin": "esUiShared", - "path": "src/plugins/es_ui_shared/public/components/view_api_request_flyout/view_api_request_flyout.tsx" - }, { "plugin": "home", "path": "src/plugins/home/public/application/application.tsx" @@ -1751,6 +1740,18 @@ "plugin": "data", "path": "src/plugins/data/public/search/session/session_indicator/connected_search_session_indicator/connected_search_session_indicator.tsx" }, + { + "plugin": "esUiShared", + "path": "src/plugins/es_ui_shared/public/components/view_api_request_flyout/view_api_request_flyout.tsx" + }, + { + "plugin": "esUiShared", + "path": "src/plugins/es_ui_shared/public/components/view_api_request_flyout/view_api_request_flyout.tsx" + }, + { + "plugin": "esUiShared", + "path": "src/plugins/es_ui_shared/public/components/view_api_request_flyout/view_api_request_flyout.tsx" + }, { "plugin": "spaces", "path": "x-pack/plugins/spaces/public/management/spaces_management_app.tsx" diff --git a/api_docs/kibana_react.mdx b/api_docs/kibana_react.mdx index d69d9e8545c969..cb02150bfad799 100644 --- a/api_docs/kibana_react.mdx +++ b/api_docs/kibana_react.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaReact title: "kibanaReact" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaReact plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaReact'] --- import kibanaReactObj from './kibana_react.devdocs.json'; diff --git a/api_docs/kibana_utils.mdx b/api_docs/kibana_utils.mdx index 169dcb7ee1f23b..8943811efb7f77 100644 --- a/api_docs/kibana_utils.mdx +++ b/api_docs/kibana_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaUtils title: "kibanaUtils" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaUtils plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaUtils'] --- import kibanaUtilsObj from './kibana_utils.devdocs.json'; diff --git a/api_docs/kubernetes_security.mdx b/api_docs/kubernetes_security.mdx index 7f3978dab8e032..42952d40cadafb 100644 --- a/api_docs/kubernetes_security.mdx +++ b/api_docs/kubernetes_security.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kubernetesSecurity title: "kubernetesSecurity" image: https://source.unsplash.com/400x175/?github description: API docs for the kubernetesSecurity plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kubernetesSecurity'] --- import kubernetesSecurityObj from './kubernetes_security.devdocs.json'; diff --git a/api_docs/lens.devdocs.json b/api_docs/lens.devdocs.json index 76f715bdb21c98..50822bc6a07feb 100644 --- a/api_docs/lens.devdocs.json +++ b/api_docs/lens.devdocs.json @@ -3594,6 +3594,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "lens", + "id": "def-public.SharedPieLayerState.collapseFns", + "type": "Object", + "tags": [], + "label": "collapseFns", + "description": [], + "signature": [ + "Record | undefined" + ], + "path": "x-pack/plugins/lens/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "lens", "id": "def-public.SharedPieLayerState.numberDisplay", @@ -10966,6 +10980,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "lens", + "id": "def-common.SharedPieLayerState.collapseFns", + "type": "Object", + "tags": [], + "label": "collapseFns", + "description": [], + "signature": [ + "Record | undefined" + ], + "path": "x-pack/plugins/lens/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "lens", "id": "def-common.SharedPieLayerState.numberDisplay", diff --git a/api_docs/lens.mdx b/api_docs/lens.mdx index a22c71323b68cd..3b1878688320a8 100644 --- a/api_docs/lens.mdx +++ b/api_docs/lens.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/lens title: "lens" image: https://source.unsplash.com/400x175/?github description: API docs for the lens plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lens'] --- import lensObj from './lens.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 639 | 0 | 550 | 41 | +| 641 | 0 | 552 | 41 | ## Client diff --git a/api_docs/license_api_guard.mdx b/api_docs/license_api_guard.mdx index 571dc8691bd0a7..35ff78f6ad4386 100644 --- a/api_docs/license_api_guard.mdx +++ b/api_docs/license_api_guard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licenseApiGuard title: "licenseApiGuard" image: https://source.unsplash.com/400x175/?github description: API docs for the licenseApiGuard plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseApiGuard'] --- import licenseApiGuardObj from './license_api_guard.devdocs.json'; diff --git a/api_docs/license_management.mdx b/api_docs/license_management.mdx index 201e1c21dffbc6..02a9e0395ee418 100644 --- a/api_docs/license_management.mdx +++ b/api_docs/license_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licenseManagement title: "licenseManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the licenseManagement plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseManagement'] --- import licenseManagementObj from './license_management.devdocs.json'; diff --git a/api_docs/licensing.mdx b/api_docs/licensing.mdx index d00e7742b2ed4a..1a45755ee8e2b9 100644 --- a/api_docs/licensing.mdx +++ b/api_docs/licensing.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licensing title: "licensing" image: https://source.unsplash.com/400x175/?github description: API docs for the licensing plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licensing'] --- import licensingObj from './licensing.devdocs.json'; diff --git a/api_docs/lists.mdx b/api_docs/lists.mdx index 751f7ca0cd136b..5883efd9fd6e9f 100644 --- a/api_docs/lists.mdx +++ b/api_docs/lists.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/lists title: "lists" image: https://source.unsplash.com/400x175/?github description: API docs for the lists plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lists'] --- import listsObj from './lists.devdocs.json'; diff --git a/api_docs/management.mdx b/api_docs/management.mdx index 9a598bdde11fb4..71270d40d9dc1f 100644 --- a/api_docs/management.mdx +++ b/api_docs/management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/management title: "management" image: https://source.unsplash.com/400x175/?github description: API docs for the management plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'management'] --- import managementObj from './management.devdocs.json'; diff --git a/api_docs/maps.devdocs.json b/api_docs/maps.devdocs.json index e4f20997296834..83836732b0bfc6 100644 --- a/api_docs/maps.devdocs.json +++ b/api_docs/maps.devdocs.json @@ -1222,7 +1222,8 @@ "label": "_setMapExtentFilter", "description": [], "signature": [ - "(() => void) & _.Cancelable" + "(() => void) & ", + "Cancelable" ], "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", "deprecated": false, diff --git a/api_docs/maps.mdx b/api_docs/maps.mdx index fb06fd880fdb03..09038246e80ffd 100644 --- a/api_docs/maps.mdx +++ b/api_docs/maps.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/maps title: "maps" image: https://source.unsplash.com/400x175/?github description: API docs for the maps plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'maps'] --- import mapsObj from './maps.devdocs.json'; diff --git a/api_docs/maps_ems.mdx b/api_docs/maps_ems.mdx index ff860e62ea1b92..fe001d5704b2c1 100644 --- a/api_docs/maps_ems.mdx +++ b/api_docs/maps_ems.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/mapsEms title: "mapsEms" image: https://source.unsplash.com/400x175/?github description: API docs for the mapsEms plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'mapsEms'] --- import mapsEmsObj from './maps_ems.devdocs.json'; diff --git a/api_docs/ml.mdx b/api_docs/ml.mdx index ed642dcfa50a63..5263748ce84a67 100644 --- a/api_docs/ml.mdx +++ b/api_docs/ml.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ml title: "ml" image: https://source.unsplash.com/400x175/?github description: API docs for the ml plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ml'] --- import mlObj from './ml.devdocs.json'; diff --git a/api_docs/monitoring.mdx b/api_docs/monitoring.mdx index ddf5e670048441..43551a9cf62ee3 100644 --- a/api_docs/monitoring.mdx +++ b/api_docs/monitoring.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/monitoring title: "monitoring" image: https://source.unsplash.com/400x175/?github description: API docs for the monitoring plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoring'] --- import monitoringObj from './monitoring.devdocs.json'; diff --git a/api_docs/monitoring_collection.devdocs.json b/api_docs/monitoring_collection.devdocs.json index dd6b8b549fecfc..5d88c99099b569 100644 --- a/api_docs/monitoring_collection.devdocs.json +++ b/api_docs/monitoring_collection.devdocs.json @@ -124,7 +124,7 @@ "label": "MonitoringCollectionConfig", "description": [], "signature": [ - "{ readonly enabled: boolean; readonly opentelemetry: Readonly<{} & { metrics: Readonly<{} & { otlp: Readonly<{ url?: string | undefined; headers?: Record | undefined; } & { exportIntervalMillis: number; logLevel: string; }>; prometheus: Readonly<{} & { enabled: boolean; }>; }>; }>; }" + "{ readonly enabled: boolean; readonly opentelemetry: Readonly<{} & { metrics: Readonly<{} & { otlp: Readonly<{ headers?: Record | undefined; url?: string | undefined; } & { exportIntervalMillis: number; logLevel: string; }>; prometheus: Readonly<{} & { enabled: boolean; }>; }>; }>; }" ], "path": "x-pack/plugins/monitoring_collection/server/config.ts", "deprecated": false, diff --git a/api_docs/monitoring_collection.mdx b/api_docs/monitoring_collection.mdx index 6fb7aa53be7c72..a55f2c7dfeb120 100644 --- a/api_docs/monitoring_collection.mdx +++ b/api_docs/monitoring_collection.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/monitoringCollection title: "monitoringCollection" image: https://source.unsplash.com/400x175/?github description: API docs for the monitoringCollection plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoringCollection'] --- import monitoringCollectionObj from './monitoring_collection.devdocs.json'; diff --git a/api_docs/navigation.mdx b/api_docs/navigation.mdx index e92a9aab1928c8..6275abb0490258 100644 --- a/api_docs/navigation.mdx +++ b/api_docs/navigation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/navigation title: "navigation" image: https://source.unsplash.com/400x175/?github description: API docs for the navigation plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'navigation'] --- import navigationObj from './navigation.devdocs.json'; diff --git a/api_docs/newsfeed.mdx b/api_docs/newsfeed.mdx index ef7c19dc1b7fea..8114fa0180f4f8 100644 --- a/api_docs/newsfeed.mdx +++ b/api_docs/newsfeed.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/newsfeed title: "newsfeed" image: https://source.unsplash.com/400x175/?github description: API docs for the newsfeed plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'newsfeed'] --- import newsfeedObj from './newsfeed.devdocs.json'; diff --git a/api_docs/observability.devdocs.json b/api_docs/observability.devdocs.json index 680c70e75c0601..b37b1655b90657 100644 --- a/api_docs/observability.devdocs.json +++ b/api_docs/observability.devdocs.json @@ -7760,7 +7760,7 @@ "section": "def-server.ObservabilityRouteHandlerResources", "text": "ObservabilityRouteHandlerResources" }, - ", { success: boolean; }, ", + ", { id: string; name: string; description: string; time_window: { duration: string; is_rolling: true; }; indicator: { type: \"slo.apm.transaction_duration\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; 'threshold.us': number; }; } | { type: \"slo.apm.transaction_error_rate\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; } & { good_status_codes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; }; }; budgeting_method: \"occurrences\"; objective: { target: number; }; settings: { destination_index?: string | undefined; }; }, ", { "pluginId": "observability", "scope": "server", @@ -7948,7 +7948,7 @@ "section": "def-server.ObservabilityRouteHandlerResources", "text": "ObservabilityRouteHandlerResources" }, - ", { success: boolean; }, ", + ", { id: string; name: string; description: string; time_window: { duration: string; is_rolling: true; }; indicator: { type: \"slo.apm.transaction_duration\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; 'threshold.us': number; }; } | { type: \"slo.apm.transaction_error_rate\"; params: { environment: string; service: string; transaction_type: string; transaction_name: string; } & { good_status_codes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; }; }; budgeting_method: \"occurrences\"; objective: { target: number; }; settings: { destination_index?: string | undefined; }; }, ", { "pluginId": "observability", "scope": "server", diff --git a/api_docs/observability.mdx b/api_docs/observability.mdx index 7a1e6c0e0601f9..b638866b67bf16 100644 --- a/api_docs/observability.mdx +++ b/api_docs/observability.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observability title: "observability" image: https://source.unsplash.com/400x175/?github description: API docs for the observability plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observability'] --- import observabilityObj from './observability.devdocs.json'; diff --git a/api_docs/osquery.devdocs.json b/api_docs/osquery.devdocs.json index f217e15c771ef0..8d45fc77730ef9 100644 --- a/api_docs/osquery.devdocs.json +++ b/api_docs/osquery.devdocs.json @@ -40,7 +40,27 @@ "label": "OsqueryAction", "description": [], "signature": [ - "((props: any) => JSX.Element) | undefined" + "((props: ", + "OsqueryActionProps", + ") => JSX.Element) | undefined" + ], + "path": "x-pack/plugins/osquery/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "osquery", + "id": "def-public.OsqueryPluginStart.LiveQueryField", + "type": "Function", + "tags": [], + "label": "LiveQueryField", + "description": [], + "signature": [ + "(({ formMethods, ...props }: ", + "LiveQueryQueryFieldProps", + " & { formMethods: ", + "UseFormReturn", + "<{ label: string; query: string; ecs_mapping: Record; }, any>; }) => JSX.Element) | undefined" ], "path": "x-pack/plugins/osquery/public/types.ts", "deprecated": false, diff --git a/api_docs/osquery.mdx b/api_docs/osquery.mdx index 8398670e49a38f..b483fdcffe8955 100644 --- a/api_docs/osquery.mdx +++ b/api_docs/osquery.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/osquery title: "osquery" image: https://source.unsplash.com/400x175/?github description: API docs for the osquery plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'osquery'] --- import osqueryObj from './osquery.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Security asset management](https://github.com/orgs/elastic/teams/securi | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 13 | 0 | 13 | 0 | +| 14 | 0 | 14 | 2 | ## Client diff --git a/api_docs/plugin_directory.mdx b/api_docs/plugin_directory.mdx index f6762a00b3cc2c..e861807fd0aba7 100644 --- a/api_docs/plugin_directory.mdx +++ b/api_docs/plugin_directory.mdx @@ -7,7 +7,7 @@ id: kibDevDocsPluginDirectory slug: /kibana-dev-docs/api-meta/plugin-api-directory title: Directory description: Directory of public APIs available through plugins or packages. -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -15,13 +15,13 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | Count | Plugins or Packages with a
public API | Number of teams | |--------------|----------|------------------------| -| 442 | 368 | 36 | +| 452 | 377 | 36 | ### Public API health stats | API Count | Any Count | Missing comments | Missing exports | |--------------|----------|-----------------|--------| -| 30641 | 180 | 20479 | 966 | +| 30771 | 179 | 20565 | 973 | ## Plugin Directory @@ -30,7 +30,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 272 | 0 | 267 | 19 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 23 | 0 | 19 | 1 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | AIOps plugin maintained by ML team. | 7 | 0 | 0 | 1 | -| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 368 | 0 | 359 | 21 | +| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 370 | 0 | 361 | 22 | | | [APM UI](https://github.com/orgs/elastic/teams/apm-ui) | The user interface for Elastic APM | 39 | 0 | 39 | 54 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 9 | 0 | 9 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Considering using bfetch capabilities when fetching large amounts of data. This services supports batching HTTP requests and streaming responses back. | 80 | 1 | 71 | 2 | @@ -41,16 +41,16 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Cloud Security Posture](https://github.com/orgs/elastic/teams/cloud-posture-security) | The cloud security posture plugin | 18 | 0 | 2 | 3 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 13 | 0 | 13 | 1 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The Controls Plugin contains embeddable components intended to create a simple query interface for end users, and a powerful editing suite that allows dashboard authors to build controls | 212 | 0 | 204 | 7 | -| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2657 | 1 | 61 | 2 | +| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2657 | 1 | 58 | 2 | | crossClusterReplication | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | | [Fleet](https://github.com/orgs/elastic/teams/fleet) | Add custom data integrations so they can be displayed in the Fleet integrations app | 102 | 0 | 83 | 1 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds the Dashboard app to Kibana | 147 | 0 | 142 | 12 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 52 | 0 | 51 | 0 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 3144 | 34 | 2444 | 23 | +| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 3132 | 33 | 2429 | 23 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | This plugin provides the ability to create data views via a modal flyout inside Kibana apps | 15 | 0 | 7 | 0 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Reusable data view field editor across Kibana | 49 | 0 | 29 | 3 | +| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Reusable data view field editor across Kibana | 60 | 0 | 30 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data view management app | 2 | 0 | 2 | 0 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 963 | 0 | 206 | 1 | +| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 966 | 0 | 208 | 1 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | The Data Visualizer tools help you understand your data, by analyzing the metrics and fields in a log file or an existing Elasticsearch index. | 28 | 3 | 24 | 1 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 10 | 0 | 8 | 2 | | | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | This plugin contains the Discover application and the saved search embeddable. | 95 | 0 | 78 | 4 | @@ -61,7 +61,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Enterprise Search](https://github.com/orgs/elastic/teams/enterprise-search-frontend) | Adds dashboards for discovering and managing Enterprise Search products. | 8 | 0 | 8 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 114 | 3 | 110 | 3 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | The Event Annotation service contains expressions for event annotations | 170 | 0 | 170 | 3 | -| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 100 | 0 | 100 | 9 | +| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 106 | 0 | 106 | 10 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'error' renderer to expressions | 17 | 0 | 15 | 2 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Expression Gauge plugin adds a `gauge` renderer and function to the expression plugin. The renderer will display the `gauge` chart. | 57 | 0 | 57 | 2 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Expression Heatmap plugin adds a `heatmap` renderer and function to the expression plugin. The renderer will display the `heatmap` chart. | 105 | 0 | 101 | 3 | @@ -80,7 +80,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Index pattern fields and ambiguous values formatters | 288 | 5 | 249 | 3 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | The file upload plugin contains components and services for uploading a file, analyzing its data, and then importing the data into an Elasticsearch index. Supported file types include CSV, TSV, newline-delimited JSON and GeoJSON. | 62 | 0 | 62 | 2 | | | [@elastic/kibana-app-services](https://github.com/orgs/elastic/teams/team:AppServicesUx) | File upload, download, sharing, and serving over HTTP implementation in Kibana. | 240 | 0 | 6 | 2 | -| | [Fleet](https://github.com/orgs/elastic/teams/fleet) | - | 970 | 3 | 873 | 10 | +| | [Fleet](https://github.com/orgs/elastic/teams/fleet) | - | 979 | 3 | 880 | 15 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 68 | 0 | 14 | 5 | | globalSearchBar | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 0 | 0 | 0 | 0 | | globalSearchProviders | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 0 | 0 | 0 | 0 | @@ -99,7 +99,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | kibanaUsageCollection | [Kibana Telemetry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 0 | 0 | 0 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 615 | 3 | 418 | 9 | | | [Security Team](https://github.com/orgs/elastic/teams/security-team) | - | 3 | 0 | 3 | 1 | -| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Visualization editor allowing to quickly and easily configure compelling visualizations to use on dashboards and canvas workpads. Exposes components to embed visualizations and link into the Lens editor from within other apps in Kibana. | 639 | 0 | 550 | 41 | +| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Visualization editor allowing to quickly and easily configure compelling visualizations to use on dashboards and canvas workpads. Exposes components to embed visualizations and link into the Lens editor from within other apps in Kibana. | 641 | 0 | 552 | 41 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 8 | 0 | 8 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 3 | 0 | 3 | 0 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 117 | 0 | 42 | 10 | @@ -114,13 +114,13 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 34 | 0 | 34 | 2 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 17 | 0 | 17 | 0 | | | [Observability UI](https://github.com/orgs/elastic/teams/observability-ui) | - | 397 | 2 | 394 | 30 | -| | [Security asset management](https://github.com/orgs/elastic/teams/security-asset-management) | - | 13 | 0 | 13 | 0 | +| | [Security asset management](https://github.com/orgs/elastic/teams/security-asset-management) | - | 14 | 0 | 14 | 2 | | painlessLab | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The Presentation Utility Plugin is a set of common, shared components and toolkits for solutions within the Presentation space, (e.g. Dashboards, Canvas). | 243 | 2 | 187 | 12 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 4 | 0 | 4 | 0 | | | [Kibana Reporting Services](https://github.com/orgs/elastic/teams/kibana-reporting-services) | Reporting Services enables applications to feature reports that the user can automate with Watcher and download later. | 36 | 0 | 16 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 21 | 0 | 21 | 0 | -| | [RAC](https://github.com/orgs/elastic/teams/rac) | - | 208 | 0 | 180 | 10 | +| | [RAC](https://github.com/orgs/elastic/teams/rac) | - | 213 | 0 | 185 | 11 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 24 | 0 | 19 | 2 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 193 | 2 | 152 | 5 | | | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 16 | 0 | 16 | 0 | @@ -139,7 +139,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides the Spaces feature, which allows saved objects to be organized into meaningful categories. | 260 | 0 | 64 | 0 | | | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 4 | 0 | 4 | 0 | | synthetics | [Uptime](https://github.com/orgs/elastic/teams/uptime) | This plugin visualizes data from Synthetics and Heartbeat, and integrates with other Observability solutions. | 0 | 0 | 0 | 0 | -| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 82 | 0 | 41 | 7 | +| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 83 | 0 | 41 | 7 | | | [Kibana Telemetry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 44 | 0 | 1 | 0 | | | [Kibana Telemetry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 31 | 0 | 26 | 6 | | | [Kibana Telemetry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 1 | 0 | 1 | 0 | @@ -152,7 +152,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds UI Actions service to Kibana | 132 | 0 | 91 | 11 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Extends UI Actions plugin with more functionality | 205 | 0 | 142 | 9 | | | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Contains functionality for the field list which can be integrated into apps | 54 | 0 | 52 | 2 | -| | [Unified Search](https://github.com/orgs/elastic/teams/kibana-app-services) | Contains all the key functionality of Kibana's unified search experience.Contains all the key functionality of Kibana's unified search experience. | 111 | 2 | 85 | 16 | +| | [Unified Search](https://github.com/orgs/elastic/teams/kibana-app-services) | Contains all the key functionality of Kibana's unified search experience.Contains all the key functionality of Kibana's unified search experience. | 122 | 2 | 96 | 16 | | upgradeAssistant | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | urlDrilldown | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds drilldown implementations to Kibana | 0 | 0 | 0 | 0 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | - | 12 | 0 | 12 | 0 | @@ -189,7 +189,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | Kibana Core | - | 18 | 0 | 0 | 0 | | | Kibana Core | - | 20 | 0 | 0 | 0 | | | [Owner missing] | - | 16 | 0 | 16 | 0 | -| | [Owner missing] | Elastic APM trace data generator | 76 | 0 | 76 | 12 | +| | [Owner missing] | Elastic APM trace data generator | 72 | 0 | 72 | 12 | | | [Owner missing] | - | 11 | 0 | 11 | 0 | | | [Owner missing] | - | 10 | 0 | 10 | 0 | | | [Owner missing] | - | 76 | 0 | 76 | 0 | @@ -211,8 +211,10 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | Kibana Core | - | 18 | 0 | 15 | 1 | | | Kibana Core | - | 12 | 0 | 12 | 0 | | | Kibana Core | - | 8 | 0 | 1 | 0 | +| | Kibana Core | - | 20 | 0 | 19 | 0 | +| | Kibana Core | - | 2 | 0 | 2 | 0 | | | Kibana Core | - | 3 | 0 | 3 | 0 | -| | Kibana Core | - | 20 | 0 | 3 | 0 | +| | Kibana Core | - | 12 | 0 | 3 | 0 | | | Kibana Core | - | 7 | 0 | 7 | 0 | | | Kibana Core | - | 3 | 0 | 3 | 0 | | | Kibana Core | - | 3 | 0 | 3 | 0 | @@ -307,6 +309,11 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | Kibana Core | - | 66 | 0 | 66 | 4 | | | Kibana Core | - | 14 | 0 | 13 | 0 | | | Kibana Core | - | 99 | 1 | 86 | 0 | +| | Kibana Core | - | 12 | 0 | 2 | 0 | +| | Kibana Core | - | 19 | 0 | 18 | 0 | +| | Kibana Core | - | 20 | 0 | 1 | 0 | +| | Kibana Core | - | 22 | 0 | 22 | 1 | +| | Kibana Core | - | 4 | 0 | 4 | 0 | | | Kibana Core | - | 11 | 0 | 9 | 0 | | | Kibana Core | - | 5 | 0 | 5 | 0 | | | Kibana Core | - | 6 | 0 | 4 | 0 | @@ -351,7 +358,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | Kibana Core | - | 8 | 0 | 8 | 0 | | | [Owner missing] | - | 6 | 0 | 1 | 1 | | | [Owner missing] | - | 534 | 1 | 1 | 0 | -| | Machine Learning UI | This package includes utility functions related to creating elasticsearch aggregation queries, data manipulation and verification. | 53 | 2 | 35 | 4 | +| | Machine Learning UI | This package includes utility functions related to creating elasticsearch aggregation queries, data manipulation and verification. | 53 | 2 | 35 | 3 | | | Machine Learning UI | A type guard to check record like object structures. | 3 | 0 | 2 | 0 | | | Machine Learning UI | Creates a deterministic number based hash out of a string. | 2 | 0 | 1 | 0 | | | [Owner missing] | - | 55 | 0 | 55 | 2 | @@ -398,6 +405,8 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Owner missing] | - | 5 | 0 | 3 | 0 | | | [Owner missing] | - | 24 | 0 | 4 | 0 | | | [Owner missing] | - | 17 | 0 | 16 | 0 | +| | [Owner missing] | - | 2 | 0 | 1 | 0 | +| | [Owner missing] | - | 1 | 0 | 1 | 0 | | | [Owner missing] | - | 2 | 0 | 0 | 0 | | | [Owner missing] | - | 14 | 0 | 4 | 1 | | | [Owner missing] | - | 9 | 0 | 3 | 0 | @@ -407,7 +416,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Owner missing] | - | 4 | 0 | 2 | 0 | | | Operations | - | 38 | 2 | 21 | 0 | | | Kibana Core | - | 2 | 0 | 2 | 0 | -| | Operations | - | 253 | 5 | 212 | 11 | +| | Operations | - | 254 | 5 | 213 | 11 | | | [Owner missing] | - | 135 | 8 | 103 | 2 | | | [Owner missing] | - | 72 | 0 | 55 | 0 | | | [Owner missing] | - | 8 | 0 | 2 | 0 | diff --git a/api_docs/presentation_util.mdx b/api_docs/presentation_util.mdx index bd88e5f88c6ffd..85cb946befa84e 100644 --- a/api_docs/presentation_util.mdx +++ b/api_docs/presentation_util.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/presentationUtil title: "presentationUtil" image: https://source.unsplash.com/400x175/?github description: API docs for the presentationUtil plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'presentationUtil'] --- import presentationUtilObj from './presentation_util.devdocs.json'; diff --git a/api_docs/remote_clusters.mdx b/api_docs/remote_clusters.mdx index 4dc8865027be98..03e74d03d9550f 100644 --- a/api_docs/remote_clusters.mdx +++ b/api_docs/remote_clusters.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/remoteClusters title: "remoteClusters" image: https://source.unsplash.com/400x175/?github description: API docs for the remoteClusters plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'remoteClusters'] --- import remoteClustersObj from './remote_clusters.devdocs.json'; diff --git a/api_docs/reporting.mdx b/api_docs/reporting.mdx index 98619fec51a28f..16b9d7d579f940 100644 --- a/api_docs/reporting.mdx +++ b/api_docs/reporting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/reporting title: "reporting" image: https://source.unsplash.com/400x175/?github description: API docs for the reporting plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'reporting'] --- import reportingObj from './reporting.devdocs.json'; diff --git a/api_docs/rollup.mdx b/api_docs/rollup.mdx index 32c76e983e404c..91f2f9238565ff 100644 --- a/api_docs/rollup.mdx +++ b/api_docs/rollup.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/rollup title: "rollup" image: https://source.unsplash.com/400x175/?github description: API docs for the rollup plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'rollup'] --- import rollupObj from './rollup.devdocs.json'; diff --git a/api_docs/rule_registry.devdocs.json b/api_docs/rule_registry.devdocs.json index dff8ff4faf897a..63cb7763de4a9d 100644 --- a/api_docs/rule_registry.devdocs.json +++ b/api_docs/rule_registry.devdocs.json @@ -401,6 +401,77 @@ } ], "returnComment": [] + }, + { + "parentPluginId": "ruleRegistry", + "id": "def-server.AlertsClient.getBrowserFields", + "type": "Function", + "tags": [], + "label": "getBrowserFields", + "description": [], + "signature": [ + "({ indices, metaFields, allowNoIndex, }: { indices: string[]; metaFields: string[]; allowNoIndex: boolean; }) => Promise<", + "BrowserFields", + ">" + ], + "path": "x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "ruleRegistry", + "id": "def-server.AlertsClient.getBrowserFields.$1", + "type": "Object", + "tags": [], + "label": "{\n indices,\n metaFields,\n allowNoIndex,\n }", + "description": [], + "path": "x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "ruleRegistry", + "id": "def-server.AlertsClient.getBrowserFields.$1.indices", + "type": "Array", + "tags": [], + "label": "indices", + "description": [], + "signature": [ + "string[]" + ], + "path": "x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "ruleRegistry", + "id": "def-server.AlertsClient.getBrowserFields.$1.metaFields", + "type": "Array", + "tags": [], + "label": "metaFields", + "description": [], + "signature": [ + "string[]" + ], + "path": "x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "ruleRegistry", + "id": "def-server.AlertsClient.getBrowserFields.$1.allowNoIndex", + "type": "boolean", + "tags": [], + "label": "allowNoIndex", + "description": [], + "path": "x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts", + "deprecated": false, + "trackAdoption": false + } + ] + } + ], + "returnComment": [] } ], "initialIsOpen": false diff --git a/api_docs/rule_registry.mdx b/api_docs/rule_registry.mdx index 0c20752827038f..417fd4d363b98a 100644 --- a/api_docs/rule_registry.mdx +++ b/api_docs/rule_registry.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ruleRegistry title: "ruleRegistry" image: https://source.unsplash.com/400x175/?github description: API docs for the ruleRegistry plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ruleRegistry'] --- import ruleRegistryObj from './rule_registry.devdocs.json'; @@ -21,7 +21,7 @@ Contact [RAC](https://github.com/orgs/elastic/teams/rac) for questions regarding | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 208 | 0 | 180 | 10 | +| 213 | 0 | 185 | 11 | ## Server diff --git a/api_docs/runtime_fields.mdx b/api_docs/runtime_fields.mdx index 3da2926bef45d4..5f4dc7a882058a 100644 --- a/api_docs/runtime_fields.mdx +++ b/api_docs/runtime_fields.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/runtimeFields title: "runtimeFields" image: https://source.unsplash.com/400x175/?github description: API docs for the runtimeFields plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'runtimeFields'] --- import runtimeFieldsObj from './runtime_fields.devdocs.json'; diff --git a/api_docs/saved_objects.devdocs.json b/api_docs/saved_objects.devdocs.json index 31a0d6adba0d91..ea2a636da67388 100644 --- a/api_docs/saved_objects.devdocs.json +++ b/api_docs/saved_objects.devdocs.json @@ -789,7 +789,7 @@ }, ", \"id\" | \"title\" | \"getDisplayName\" | \"lastSavedTitle\" | \"copyOnSave\" | \"getEsType\">, isTitleDuplicateConfirmed: boolean, onTitleDuplicate: (() => void) | undefined, services: Pick<", "SavedObjectKibanaServices", - ", \"overlays\" | \"savedObjectsClient\">) => Promise" + ", \"savedObjectsClient\" | \"overlays\">) => Promise" ], "path": "src/plugins/saved_objects/public/saved_object/helpers/check_for_duplicate_title.ts", "deprecated": false, @@ -858,7 +858,7 @@ "signature": [ "Pick<", "SavedObjectKibanaServices", - ", \"overlays\" | \"savedObjectsClient\">" + ", \"savedObjectsClient\" | \"overlays\">" ], "path": "src/plugins/saved_objects/public/saved_object/helpers/check_for_duplicate_title.ts", "deprecated": false, diff --git a/api_docs/saved_objects.mdx b/api_docs/saved_objects.mdx index ccb2436bc7ab51..8df5077867d98b 100644 --- a/api_docs/saved_objects.mdx +++ b/api_docs/saved_objects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjects title: "savedObjects" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjects plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjects'] --- import savedObjectsObj from './saved_objects.devdocs.json'; diff --git a/api_docs/saved_objects_finder.mdx b/api_docs/saved_objects_finder.mdx index 8ebb9e956a9bd1..611cf2a98de70a 100644 --- a/api_docs/saved_objects_finder.mdx +++ b/api_docs/saved_objects_finder.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsFinder title: "savedObjectsFinder" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsFinder plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsFinder'] --- import savedObjectsFinderObj from './saved_objects_finder.devdocs.json'; diff --git a/api_docs/saved_objects_management.devdocs.json b/api_docs/saved_objects_management.devdocs.json index f148fcdfe865fd..2cd75f6e2bb2fd 100644 --- a/api_docs/saved_objects_management.devdocs.json +++ b/api_docs/saved_objects_management.devdocs.json @@ -310,7 +310,7 @@ "section": "def-public.SavedObjectsManagementRecord", "text": "SavedObjectsManagementRecord" }, - "; defaultValue?: string | number | readonly string[] | undefined; lang?: string | undefined; defaultChecked?: boolean | undefined; suppressContentEditableWarning?: boolean | undefined; suppressHydrationWarning?: boolean | undefined; accessKey?: string | undefined; contentEditable?: \"inherit\" | Booleanish | undefined; contextMenu?: string | undefined; dir?: string | undefined; draggable?: Booleanish | undefined; placeholder?: string | undefined; slot?: string | undefined; spellCheck?: Booleanish | undefined; style?: React.CSSProperties | undefined; tabIndex?: number | undefined; translate?: \"no\" | \"yes\" | undefined; radioGroup?: string | undefined; role?: React.AriaRole | undefined; about?: string | undefined; datatype?: string | undefined; inlist?: any; prefix?: string | undefined; property?: string | undefined; resource?: string | undefined; typeof?: string | undefined; vocab?: string | undefined; autoCapitalize?: string | undefined; autoCorrect?: string | undefined; autoSave?: string | undefined; itemProp?: string | undefined; itemScope?: boolean | undefined; itemType?: string | undefined; itemID?: string | undefined; itemRef?: string | undefined; results?: number | undefined; unselectable?: \"on\" | \"off\" | undefined; inputMode?: \"none\" | \"email\" | \"search\" | \"text\" | \"tel\" | \"url\" | \"numeric\" | \"decimal\" | undefined; is?: string | undefined; 'aria-activedescendant'?: string | undefined; 'aria-atomic'?: Booleanish | undefined; 'aria-autocomplete'?: \"none\" | \"list\" | \"inline\" | \"both\" | undefined; 'aria-busy'?: Booleanish | undefined; 'aria-checked'?: boolean | \"mixed\" | \"false\" | \"true\" | undefined; 'aria-colcount'?: number | undefined; 'aria-colindex'?: number | undefined; 'aria-colspan'?: number | undefined; 'aria-controls'?: string | undefined; 'aria-current'?: boolean | \"date\" | \"location\" | \"time\" | \"page\" | \"false\" | \"true\" | \"step\" | undefined; 'aria-describedby'?: string | undefined; 'aria-details'?: string | undefined; 'aria-disabled'?: Booleanish | undefined; 'aria-dropeffect'?: \"none\" | \"copy\" | \"link\" | \"execute\" | \"move\" | \"popup\" | undefined; 'aria-errormessage'?: string | undefined; 'aria-expanded'?: Booleanish | undefined; 'aria-flowto'?: string | undefined; 'aria-grabbed'?: Booleanish | undefined; 'aria-haspopup'?: boolean | \"grid\" | \"menu\" | \"false\" | \"true\" | \"dialog\" | \"listbox\" | \"tree\" | undefined; 'aria-hidden'?: Booleanish | undefined; 'aria-invalid'?: boolean | \"false\" | \"true\" | \"grammar\" | \"spelling\" | undefined; 'aria-keyshortcuts'?: string | undefined; 'aria-label'?: string | undefined; 'aria-labelledby'?: string | undefined; 'aria-level'?: number | undefined; 'aria-live'?: \"off\" | \"assertive\" | \"polite\" | undefined; 'aria-modal'?: Booleanish | undefined; 'aria-multiline'?: Booleanish | undefined; 'aria-multiselectable'?: Booleanish | undefined; 'aria-orientation'?: \"horizontal\" | \"vertical\" | undefined; 'aria-owns'?: string | undefined; 'aria-placeholder'?: string | undefined; 'aria-posinset'?: number | undefined; 'aria-pressed'?: boolean | \"mixed\" | \"false\" | \"true\" | undefined; 'aria-readonly'?: Booleanish | undefined; 'aria-relevant'?: \"all\" | \"text\" | \"additions\" | \"additions removals\" | \"additions text\" | \"removals\" | \"removals additions\" | \"removals text\" | \"text additions\" | \"text removals\" | undefined; 'aria-required'?: Booleanish | undefined; 'aria-roledescription'?: string | undefined; 'aria-rowcount'?: number | undefined; 'aria-rowindex'?: number | undefined; 'aria-rowspan'?: number | undefined; 'aria-selected'?: Booleanish | undefined; 'aria-setsize'?: number | undefined; 'aria-sort'?: \"none\" | \"other\" | \"ascending\" | \"descending\" | undefined; 'aria-valuemax'?: number | undefined; 'aria-valuemin'?: number | undefined; 'aria-valuenow'?: number | undefined; 'aria-valuetext'?: string | undefined; dangerouslySetInnerHTML?: { __html: string; } | undefined; onCopy?: React.ClipboardEventHandler | undefined; onCopyCapture?: React.ClipboardEventHandler | undefined; onCut?: React.ClipboardEventHandler | undefined; onCutCapture?: React.ClipboardEventHandler | undefined; onPaste?: React.ClipboardEventHandler | undefined; onPasteCapture?: React.ClipboardEventHandler | undefined; onCompositionEnd?: React.CompositionEventHandler | undefined; onCompositionEndCapture?: React.CompositionEventHandler | undefined; onCompositionStart?: React.CompositionEventHandler | undefined; onCompositionStartCapture?: React.CompositionEventHandler | undefined; onCompositionUpdate?: React.CompositionEventHandler | undefined; onCompositionUpdateCapture?: React.CompositionEventHandler | undefined; onFocus?: React.FocusEventHandler | undefined; onFocusCapture?: React.FocusEventHandler | undefined; onBlur?: React.FocusEventHandler | undefined; onBlurCapture?: React.FocusEventHandler | undefined; onChangeCapture?: React.FormEventHandler | undefined; onBeforeInput?: React.FormEventHandler | undefined; onBeforeInputCapture?: React.FormEventHandler | undefined; onInput?: React.FormEventHandler | undefined; onInputCapture?: React.FormEventHandler | undefined; onReset?: React.FormEventHandler | undefined; onResetCapture?: React.FormEventHandler | undefined; onSubmit?: React.FormEventHandler | undefined; onSubmitCapture?: React.FormEventHandler | undefined; onInvalid?: React.FormEventHandler | undefined; onInvalidCapture?: React.FormEventHandler | undefined; onLoad?: React.ReactEventHandler | undefined; onLoadCapture?: React.ReactEventHandler | undefined; onErrorCapture?: React.ReactEventHandler | undefined; onKeyDownCapture?: React.KeyboardEventHandler | undefined; onKeyPress?: React.KeyboardEventHandler | undefined; onKeyPressCapture?: React.KeyboardEventHandler | undefined; onKeyUp?: React.KeyboardEventHandler | undefined; onKeyUpCapture?: React.KeyboardEventHandler | undefined; onAbort?: React.ReactEventHandler | undefined; onAbortCapture?: React.ReactEventHandler | undefined; onCanPlay?: React.ReactEventHandler | undefined; onCanPlayCapture?: React.ReactEventHandler | undefined; onCanPlayThrough?: React.ReactEventHandler | undefined; onCanPlayThroughCapture?: React.ReactEventHandler | undefined; onDurationChange?: React.ReactEventHandler | undefined; onDurationChangeCapture?: React.ReactEventHandler | undefined; onEmptied?: React.ReactEventHandler | undefined; onEmptiedCapture?: React.ReactEventHandler | undefined; onEncrypted?: React.ReactEventHandler | undefined; onEncryptedCapture?: React.ReactEventHandler | undefined; onEnded?: React.ReactEventHandler | undefined; onEndedCapture?: React.ReactEventHandler | undefined; onLoadedData?: React.ReactEventHandler | undefined; onLoadedDataCapture?: React.ReactEventHandler | undefined; onLoadedMetadata?: React.ReactEventHandler | undefined; onLoadedMetadataCapture?: React.ReactEventHandler | undefined; onLoadStart?: React.ReactEventHandler | undefined; onLoadStartCapture?: React.ReactEventHandler | undefined; onPause?: React.ReactEventHandler | undefined; onPauseCapture?: React.ReactEventHandler | undefined; onPlay?: React.ReactEventHandler | undefined; onPlayCapture?: React.ReactEventHandler | undefined; onPlaying?: React.ReactEventHandler | undefined; onPlayingCapture?: React.ReactEventHandler | undefined; onProgress?: React.ReactEventHandler | undefined; onProgressCapture?: React.ReactEventHandler | undefined; onRateChange?: React.ReactEventHandler | undefined; onRateChangeCapture?: React.ReactEventHandler | undefined; onSeeked?: React.ReactEventHandler | undefined; onSeekedCapture?: React.ReactEventHandler | undefined; onSeeking?: React.ReactEventHandler | undefined; onSeekingCapture?: React.ReactEventHandler | undefined; onStalled?: React.ReactEventHandler | undefined; onStalledCapture?: React.ReactEventHandler | undefined; onSuspend?: React.ReactEventHandler | undefined; onSuspendCapture?: React.ReactEventHandler | undefined; onTimeUpdate?: React.ReactEventHandler | undefined; onTimeUpdateCapture?: React.ReactEventHandler | undefined; onVolumeChange?: React.ReactEventHandler | undefined; onVolumeChangeCapture?: React.ReactEventHandler | undefined; onWaiting?: React.ReactEventHandler | undefined; onWaitingCapture?: React.ReactEventHandler | undefined; onAuxClick?: React.MouseEventHandler | undefined; onAuxClickCapture?: React.MouseEventHandler | undefined; onClickCapture?: React.MouseEventHandler | undefined; onContextMenu?: React.MouseEventHandler | undefined; onContextMenuCapture?: React.MouseEventHandler | undefined; onDoubleClick?: React.MouseEventHandler | undefined; onDoubleClickCapture?: React.MouseEventHandler | undefined; onDrag?: React.DragEventHandler | undefined; onDragCapture?: React.DragEventHandler | undefined; onDragEnd?: React.DragEventHandler | undefined; onDragEndCapture?: React.DragEventHandler | undefined; onDragEnter?: React.DragEventHandler | undefined; onDragEnterCapture?: React.DragEventHandler | undefined; onDragExit?: React.DragEventHandler | undefined; onDragExitCapture?: React.DragEventHandler | undefined; onDragLeave?: React.DragEventHandler | undefined; onDragLeaveCapture?: React.DragEventHandler | undefined; onDragOver?: React.DragEventHandler | undefined; onDragOverCapture?: React.DragEventHandler | undefined; onDragStart?: React.DragEventHandler | undefined; onDragStartCapture?: React.DragEventHandler | undefined; onDrop?: React.DragEventHandler | undefined; onDropCapture?: React.DragEventHandler | undefined; onMouseDown?: React.MouseEventHandler | undefined; onMouseDownCapture?: React.MouseEventHandler | undefined; onMouseEnter?: React.MouseEventHandler | undefined; onMouseLeave?: React.MouseEventHandler | undefined; onMouseMove?: React.MouseEventHandler | undefined; onMouseMoveCapture?: React.MouseEventHandler | undefined; onMouseOut?: React.MouseEventHandler | undefined; onMouseOutCapture?: React.MouseEventHandler | undefined; onMouseOver?: React.MouseEventHandler | undefined; onMouseOverCapture?: React.MouseEventHandler | undefined; onMouseUp?: React.MouseEventHandler | undefined; onMouseUpCapture?: React.MouseEventHandler | undefined; onSelect?: React.ReactEventHandler | undefined; onSelectCapture?: React.ReactEventHandler | undefined; onTouchCancel?: React.TouchEventHandler | undefined; onTouchCancelCapture?: React.TouchEventHandler | undefined; onTouchEnd?: React.TouchEventHandler | undefined; onTouchEndCapture?: React.TouchEventHandler | undefined; onTouchMove?: React.TouchEventHandler | undefined; onTouchMoveCapture?: React.TouchEventHandler | undefined; onTouchStart?: React.TouchEventHandler | undefined; onTouchStartCapture?: React.TouchEventHandler | undefined; onPointerDown?: React.PointerEventHandler | undefined; onPointerDownCapture?: React.PointerEventHandler | undefined; onPointerMove?: React.PointerEventHandler | undefined; onPointerMoveCapture?: React.PointerEventHandler | undefined; onPointerUp?: React.PointerEventHandler | undefined; onPointerUpCapture?: React.PointerEventHandler | undefined; onPointerCancel?: React.PointerEventHandler | undefined; onPointerCancelCapture?: React.PointerEventHandler | undefined; onPointerEnter?: React.PointerEventHandler | undefined; onPointerEnterCapture?: React.PointerEventHandler | undefined; onPointerLeave?: React.PointerEventHandler | undefined; onPointerLeaveCapture?: React.PointerEventHandler | undefined; onPointerOver?: React.PointerEventHandler | undefined; onPointerOverCapture?: React.PointerEventHandler | undefined; onPointerOut?: React.PointerEventHandler | undefined; onPointerOutCapture?: React.PointerEventHandler | undefined; onGotPointerCapture?: React.PointerEventHandler | undefined; onGotPointerCaptureCapture?: React.PointerEventHandler | undefined; onLostPointerCapture?: React.PointerEventHandler | undefined; onLostPointerCaptureCapture?: React.PointerEventHandler | undefined; onScroll?: React.UIEventHandler | undefined; onScrollCapture?: React.UIEventHandler | undefined; onWheel?: React.WheelEventHandler | undefined; onWheelCapture?: React.WheelEventHandler | undefined; onAnimationStart?: React.AnimationEventHandler | undefined; onAnimationStartCapture?: React.AnimationEventHandler | undefined; onAnimationEnd?: React.AnimationEventHandler | undefined; onAnimationEndCapture?: React.AnimationEventHandler | undefined; onAnimationIteration?: React.AnimationEventHandler | undefined; onAnimationIterationCapture?: React.AnimationEventHandler | undefined; onTransitionEnd?: React.TransitionEventHandler | undefined; onTransitionEndCapture?: React.TransitionEventHandler | undefined; 'data-test-subj'?: string | undefined; height?: string | number | undefined; width?: string | undefined; readOnly?: boolean | undefined; align?: ", + "; headers?: string | undefined; defaultValue?: string | number | readonly string[] | undefined; lang?: string | undefined; defaultChecked?: boolean | undefined; suppressContentEditableWarning?: boolean | undefined; suppressHydrationWarning?: boolean | undefined; accessKey?: string | undefined; contentEditable?: \"inherit\" | Booleanish | undefined; contextMenu?: string | undefined; dir?: string | undefined; draggable?: Booleanish | undefined; placeholder?: string | undefined; slot?: string | undefined; spellCheck?: Booleanish | undefined; style?: React.CSSProperties | undefined; tabIndex?: number | undefined; translate?: \"no\" | \"yes\" | undefined; radioGroup?: string | undefined; role?: React.AriaRole | undefined; about?: string | undefined; datatype?: string | undefined; inlist?: any; prefix?: string | undefined; property?: string | undefined; resource?: string | undefined; typeof?: string | undefined; vocab?: string | undefined; autoCapitalize?: string | undefined; autoCorrect?: string | undefined; autoSave?: string | undefined; itemProp?: string | undefined; itemScope?: boolean | undefined; itemType?: string | undefined; itemID?: string | undefined; itemRef?: string | undefined; results?: number | undefined; unselectable?: \"on\" | \"off\" | undefined; inputMode?: \"none\" | \"email\" | \"search\" | \"text\" | \"tel\" | \"url\" | \"numeric\" | \"decimal\" | undefined; is?: string | undefined; 'aria-activedescendant'?: string | undefined; 'aria-atomic'?: Booleanish | undefined; 'aria-autocomplete'?: \"none\" | \"list\" | \"inline\" | \"both\" | undefined; 'aria-busy'?: Booleanish | undefined; 'aria-checked'?: boolean | \"mixed\" | \"false\" | \"true\" | undefined; 'aria-colcount'?: number | undefined; 'aria-colindex'?: number | undefined; 'aria-colspan'?: number | undefined; 'aria-controls'?: string | undefined; 'aria-current'?: boolean | \"date\" | \"location\" | \"time\" | \"page\" | \"false\" | \"true\" | \"step\" | undefined; 'aria-describedby'?: string | undefined; 'aria-details'?: string | undefined; 'aria-disabled'?: Booleanish | undefined; 'aria-dropeffect'?: \"none\" | \"copy\" | \"link\" | \"execute\" | \"move\" | \"popup\" | undefined; 'aria-errormessage'?: string | undefined; 'aria-expanded'?: Booleanish | undefined; 'aria-flowto'?: string | undefined; 'aria-grabbed'?: Booleanish | undefined; 'aria-haspopup'?: boolean | \"grid\" | \"menu\" | \"false\" | \"true\" | \"dialog\" | \"listbox\" | \"tree\" | undefined; 'aria-hidden'?: Booleanish | undefined; 'aria-invalid'?: boolean | \"false\" | \"true\" | \"grammar\" | \"spelling\" | undefined; 'aria-keyshortcuts'?: string | undefined; 'aria-label'?: string | undefined; 'aria-labelledby'?: string | undefined; 'aria-level'?: number | undefined; 'aria-live'?: \"off\" | \"assertive\" | \"polite\" | undefined; 'aria-modal'?: Booleanish | undefined; 'aria-multiline'?: Booleanish | undefined; 'aria-multiselectable'?: Booleanish | undefined; 'aria-orientation'?: \"horizontal\" | \"vertical\" | undefined; 'aria-owns'?: string | undefined; 'aria-placeholder'?: string | undefined; 'aria-posinset'?: number | undefined; 'aria-pressed'?: boolean | \"mixed\" | \"false\" | \"true\" | undefined; 'aria-readonly'?: Booleanish | undefined; 'aria-relevant'?: \"all\" | \"text\" | \"additions\" | \"additions removals\" | \"additions text\" | \"removals\" | \"removals additions\" | \"removals text\" | \"text additions\" | \"text removals\" | undefined; 'aria-required'?: Booleanish | undefined; 'aria-roledescription'?: string | undefined; 'aria-rowcount'?: number | undefined; 'aria-rowindex'?: number | undefined; 'aria-rowspan'?: number | undefined; 'aria-selected'?: Booleanish | undefined; 'aria-setsize'?: number | undefined; 'aria-sort'?: \"none\" | \"other\" | \"ascending\" | \"descending\" | undefined; 'aria-valuemax'?: number | undefined; 'aria-valuemin'?: number | undefined; 'aria-valuenow'?: number | undefined; 'aria-valuetext'?: string | undefined; dangerouslySetInnerHTML?: { __html: string; } | undefined; onCopy?: React.ClipboardEventHandler | undefined; onCopyCapture?: React.ClipboardEventHandler | undefined; onCut?: React.ClipboardEventHandler | undefined; onCutCapture?: React.ClipboardEventHandler | undefined; onPaste?: React.ClipboardEventHandler | undefined; onPasteCapture?: React.ClipboardEventHandler | undefined; onCompositionEnd?: React.CompositionEventHandler | undefined; onCompositionEndCapture?: React.CompositionEventHandler | undefined; onCompositionStart?: React.CompositionEventHandler | undefined; onCompositionStartCapture?: React.CompositionEventHandler | undefined; onCompositionUpdate?: React.CompositionEventHandler | undefined; onCompositionUpdateCapture?: React.CompositionEventHandler | undefined; onFocus?: React.FocusEventHandler | undefined; onFocusCapture?: React.FocusEventHandler | undefined; onBlur?: React.FocusEventHandler | undefined; onBlurCapture?: React.FocusEventHandler | undefined; onChangeCapture?: React.FormEventHandler | undefined; onBeforeInput?: React.FormEventHandler | undefined; onBeforeInputCapture?: React.FormEventHandler | undefined; onInput?: React.FormEventHandler | undefined; onInputCapture?: React.FormEventHandler | undefined; onReset?: React.FormEventHandler | undefined; onResetCapture?: React.FormEventHandler | undefined; onSubmit?: React.FormEventHandler | undefined; onSubmitCapture?: React.FormEventHandler | undefined; onInvalid?: React.FormEventHandler | undefined; onInvalidCapture?: React.FormEventHandler | undefined; onLoad?: React.ReactEventHandler | undefined; onLoadCapture?: React.ReactEventHandler | undefined; onErrorCapture?: React.ReactEventHandler | undefined; onKeyDownCapture?: React.KeyboardEventHandler | undefined; onKeyPress?: React.KeyboardEventHandler | undefined; onKeyPressCapture?: React.KeyboardEventHandler | undefined; onKeyUp?: React.KeyboardEventHandler | undefined; onKeyUpCapture?: React.KeyboardEventHandler | undefined; onAbort?: React.ReactEventHandler | undefined; onAbortCapture?: React.ReactEventHandler | undefined; onCanPlay?: React.ReactEventHandler | undefined; onCanPlayCapture?: React.ReactEventHandler | undefined; onCanPlayThrough?: React.ReactEventHandler | undefined; onCanPlayThroughCapture?: React.ReactEventHandler | undefined; onDurationChange?: React.ReactEventHandler | undefined; onDurationChangeCapture?: React.ReactEventHandler | undefined; onEmptied?: React.ReactEventHandler | undefined; onEmptiedCapture?: React.ReactEventHandler | undefined; onEncrypted?: React.ReactEventHandler | undefined; onEncryptedCapture?: React.ReactEventHandler | undefined; onEnded?: React.ReactEventHandler | undefined; onEndedCapture?: React.ReactEventHandler | undefined; onLoadedData?: React.ReactEventHandler | undefined; onLoadedDataCapture?: React.ReactEventHandler | undefined; onLoadedMetadata?: React.ReactEventHandler | undefined; onLoadedMetadataCapture?: React.ReactEventHandler | undefined; onLoadStart?: React.ReactEventHandler | undefined; onLoadStartCapture?: React.ReactEventHandler | undefined; onPause?: React.ReactEventHandler | undefined; onPauseCapture?: React.ReactEventHandler | undefined; onPlay?: React.ReactEventHandler | undefined; onPlayCapture?: React.ReactEventHandler | undefined; onPlaying?: React.ReactEventHandler | undefined; onPlayingCapture?: React.ReactEventHandler | undefined; onProgress?: React.ReactEventHandler | undefined; onProgressCapture?: React.ReactEventHandler | undefined; onRateChange?: React.ReactEventHandler | undefined; onRateChangeCapture?: React.ReactEventHandler | undefined; onSeeked?: React.ReactEventHandler | undefined; onSeekedCapture?: React.ReactEventHandler | undefined; onSeeking?: React.ReactEventHandler | undefined; onSeekingCapture?: React.ReactEventHandler | undefined; onStalled?: React.ReactEventHandler | undefined; onStalledCapture?: React.ReactEventHandler | undefined; onSuspend?: React.ReactEventHandler | undefined; onSuspendCapture?: React.ReactEventHandler | undefined; onTimeUpdate?: React.ReactEventHandler | undefined; onTimeUpdateCapture?: React.ReactEventHandler | undefined; onVolumeChange?: React.ReactEventHandler | undefined; onVolumeChangeCapture?: React.ReactEventHandler | undefined; onWaiting?: React.ReactEventHandler | undefined; onWaitingCapture?: React.ReactEventHandler | undefined; onAuxClick?: React.MouseEventHandler | undefined; onAuxClickCapture?: React.MouseEventHandler | undefined; onClickCapture?: React.MouseEventHandler | undefined; onContextMenu?: React.MouseEventHandler | undefined; onContextMenuCapture?: React.MouseEventHandler | undefined; onDoubleClick?: React.MouseEventHandler | undefined; onDoubleClickCapture?: React.MouseEventHandler | undefined; onDrag?: React.DragEventHandler | undefined; onDragCapture?: React.DragEventHandler | undefined; onDragEnd?: React.DragEventHandler | undefined; onDragEndCapture?: React.DragEventHandler | undefined; onDragEnter?: React.DragEventHandler | undefined; onDragEnterCapture?: React.DragEventHandler | undefined; onDragExit?: React.DragEventHandler | undefined; onDragExitCapture?: React.DragEventHandler | undefined; onDragLeave?: React.DragEventHandler | undefined; onDragLeaveCapture?: React.DragEventHandler | undefined; onDragOver?: React.DragEventHandler | undefined; onDragOverCapture?: React.DragEventHandler | undefined; onDragStart?: React.DragEventHandler | undefined; onDragStartCapture?: React.DragEventHandler | undefined; onDrop?: React.DragEventHandler | undefined; onDropCapture?: React.DragEventHandler | undefined; onMouseDown?: React.MouseEventHandler | undefined; onMouseDownCapture?: React.MouseEventHandler | undefined; onMouseEnter?: React.MouseEventHandler | undefined; onMouseLeave?: React.MouseEventHandler | undefined; onMouseMove?: React.MouseEventHandler | undefined; onMouseMoveCapture?: React.MouseEventHandler | undefined; onMouseOut?: React.MouseEventHandler | undefined; onMouseOutCapture?: React.MouseEventHandler | undefined; onMouseOver?: React.MouseEventHandler | undefined; onMouseOverCapture?: React.MouseEventHandler | undefined; onMouseUp?: React.MouseEventHandler | undefined; onMouseUpCapture?: React.MouseEventHandler | undefined; onSelect?: React.ReactEventHandler | undefined; onSelectCapture?: React.ReactEventHandler | undefined; onTouchCancel?: React.TouchEventHandler | undefined; onTouchCancelCapture?: React.TouchEventHandler | undefined; onTouchEnd?: React.TouchEventHandler | undefined; onTouchEndCapture?: React.TouchEventHandler | undefined; onTouchMove?: React.TouchEventHandler | undefined; onTouchMoveCapture?: React.TouchEventHandler | undefined; onTouchStart?: React.TouchEventHandler | undefined; onTouchStartCapture?: React.TouchEventHandler | undefined; onPointerDown?: React.PointerEventHandler | undefined; onPointerDownCapture?: React.PointerEventHandler | undefined; onPointerMove?: React.PointerEventHandler | undefined; onPointerMoveCapture?: React.PointerEventHandler | undefined; onPointerUp?: React.PointerEventHandler | undefined; onPointerUpCapture?: React.PointerEventHandler | undefined; onPointerCancel?: React.PointerEventHandler | undefined; onPointerCancelCapture?: React.PointerEventHandler | undefined; onPointerEnter?: React.PointerEventHandler | undefined; onPointerEnterCapture?: React.PointerEventHandler | undefined; onPointerLeave?: React.PointerEventHandler | undefined; onPointerLeaveCapture?: React.PointerEventHandler | undefined; onPointerOver?: React.PointerEventHandler | undefined; onPointerOverCapture?: React.PointerEventHandler | undefined; onPointerOut?: React.PointerEventHandler | undefined; onPointerOutCapture?: React.PointerEventHandler | undefined; onGotPointerCapture?: React.PointerEventHandler | undefined; onGotPointerCaptureCapture?: React.PointerEventHandler | undefined; onLostPointerCapture?: React.PointerEventHandler | undefined; onLostPointerCaptureCapture?: React.PointerEventHandler | undefined; onScroll?: React.UIEventHandler | undefined; onScrollCapture?: React.UIEventHandler | undefined; onWheel?: React.WheelEventHandler | undefined; onWheelCapture?: React.WheelEventHandler | undefined; onAnimationStart?: React.AnimationEventHandler | undefined; onAnimationStartCapture?: React.AnimationEventHandler | undefined; onAnimationEnd?: React.AnimationEventHandler | undefined; onAnimationEndCapture?: React.AnimationEventHandler | undefined; onAnimationIteration?: React.AnimationEventHandler | undefined; onAnimationIterationCapture?: React.AnimationEventHandler | undefined; onTransitionEnd?: React.TransitionEventHandler | undefined; onTransitionEndCapture?: React.TransitionEventHandler | undefined; 'data-test-subj'?: string | undefined; height?: string | number | undefined; width?: string | undefined; readOnly?: boolean | undefined; align?: ", "HorizontalAlignment", " | undefined; abbr?: string | undefined; footer?: string | React.ReactElement> | ((props: ", "EuiTableFooterProps", @@ -322,7 +322,7 @@ "section": "def-public.SavedObjectsManagementRecord", "text": "SavedObjectsManagementRecord" }, - ">) => React.ReactNode) | undefined; colSpan?: number | undefined; headers?: string | undefined; rowSpan?: number | undefined; scope?: string | undefined; valign?: \"top\" | \"bottom\" | \"middle\" | \"baseline\" | undefined; dataType?: ", + ">) => React.ReactNode) | undefined; colSpan?: number | undefined; rowSpan?: number | undefined; scope?: string | undefined; valign?: \"top\" | \"bottom\" | \"middle\" | \"baseline\" | undefined; dataType?: ", "EuiTableDataType", " | undefined; isExpander?: boolean | undefined; textOnly?: boolean | undefined; truncateText?: boolean | undefined; mobileOptions?: (Omit<", "EuiTableRowCellMobileOptionsShape", diff --git a/api_docs/saved_objects_management.mdx b/api_docs/saved_objects_management.mdx index 96fc861f282ecf..093ab0cd3411fb 100644 --- a/api_docs/saved_objects_management.mdx +++ b/api_docs/saved_objects_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsManagement title: "savedObjectsManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsManagement plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsManagement'] --- import savedObjectsManagementObj from './saved_objects_management.devdocs.json'; diff --git a/api_docs/saved_objects_tagging.mdx b/api_docs/saved_objects_tagging.mdx index f1f07a53b63c0d..06500b90bde55d 100644 --- a/api_docs/saved_objects_tagging.mdx +++ b/api_docs/saved_objects_tagging.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsTagging title: "savedObjectsTagging" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsTagging plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTagging'] --- import savedObjectsTaggingObj from './saved_objects_tagging.devdocs.json'; diff --git a/api_docs/saved_objects_tagging_oss.mdx b/api_docs/saved_objects_tagging_oss.mdx index 10469dabcc739c..5a6c16542494db 100644 --- a/api_docs/saved_objects_tagging_oss.mdx +++ b/api_docs/saved_objects_tagging_oss.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsTaggingOss title: "savedObjectsTaggingOss" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsTaggingOss plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTaggingOss'] --- import savedObjectsTaggingOssObj from './saved_objects_tagging_oss.devdocs.json'; diff --git a/api_docs/saved_search.mdx b/api_docs/saved_search.mdx index e0fd13863fddb3..d801ecf01425b6 100644 --- a/api_docs/saved_search.mdx +++ b/api_docs/saved_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedSearch title: "savedSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the savedSearch plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedSearch'] --- import savedSearchObj from './saved_search.devdocs.json'; diff --git a/api_docs/screenshot_mode.mdx b/api_docs/screenshot_mode.mdx index ab9308881edad5..33aa8147cd15cf 100644 --- a/api_docs/screenshot_mode.mdx +++ b/api_docs/screenshot_mode.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/screenshotMode title: "screenshotMode" image: https://source.unsplash.com/400x175/?github description: API docs for the screenshotMode plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotMode'] --- import screenshotModeObj from './screenshot_mode.devdocs.json'; diff --git a/api_docs/screenshotting.mdx b/api_docs/screenshotting.mdx index 25835a28fb913b..da135809842195 100644 --- a/api_docs/screenshotting.mdx +++ b/api_docs/screenshotting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/screenshotting title: "screenshotting" image: https://source.unsplash.com/400x175/?github description: API docs for the screenshotting plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotting'] --- import screenshottingObj from './screenshotting.devdocs.json'; diff --git a/api_docs/security.mdx b/api_docs/security.mdx index 7aeaddd49f9fc2..f1603607e7234f 100644 --- a/api_docs/security.mdx +++ b/api_docs/security.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/security title: "security" image: https://source.unsplash.com/400x175/?github description: API docs for the security plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'security'] --- import securityObj from './security.devdocs.json'; diff --git a/api_docs/security_solution.mdx b/api_docs/security_solution.mdx index f2e9de3b4461d9..14a2b8cc373f60 100644 --- a/api_docs/security_solution.mdx +++ b/api_docs/security_solution.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/securitySolution title: "securitySolution" image: https://source.unsplash.com/400x175/?github description: API docs for the securitySolution plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'securitySolution'] --- import securitySolutionObj from './security_solution.devdocs.json'; diff --git a/api_docs/session_view.mdx b/api_docs/session_view.mdx index a0b46b34aff8c2..4f197b0a9fce09 100644 --- a/api_docs/session_view.mdx +++ b/api_docs/session_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/sessionView title: "sessionView" image: https://source.unsplash.com/400x175/?github description: API docs for the sessionView plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'sessionView'] --- import sessionViewObj from './session_view.devdocs.json'; diff --git a/api_docs/share.mdx b/api_docs/share.mdx index 917ac7b5c8b7aa..71f2d14dd7453c 100644 --- a/api_docs/share.mdx +++ b/api_docs/share.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/share title: "share" image: https://source.unsplash.com/400x175/?github description: API docs for the share plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'share'] --- import shareObj from './share.devdocs.json'; diff --git a/api_docs/snapshot_restore.mdx b/api_docs/snapshot_restore.mdx index 3d2d4e9f7d91bb..ba5838205eb109 100644 --- a/api_docs/snapshot_restore.mdx +++ b/api_docs/snapshot_restore.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/snapshotRestore title: "snapshotRestore" image: https://source.unsplash.com/400x175/?github description: API docs for the snapshotRestore plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'snapshotRestore'] --- import snapshotRestoreObj from './snapshot_restore.devdocs.json'; diff --git a/api_docs/spaces.mdx b/api_docs/spaces.mdx index 039252a8b5f89e..73c9f72a12d4c0 100644 --- a/api_docs/spaces.mdx +++ b/api_docs/spaces.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/spaces title: "spaces" image: https://source.unsplash.com/400x175/?github description: API docs for the spaces plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'spaces'] --- import spacesObj from './spaces.devdocs.json'; diff --git a/api_docs/stack_alerts.mdx b/api_docs/stack_alerts.mdx index e1bbb20979c05e..597973bf7ee2f8 100644 --- a/api_docs/stack_alerts.mdx +++ b/api_docs/stack_alerts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/stackAlerts title: "stackAlerts" image: https://source.unsplash.com/400x175/?github description: API docs for the stackAlerts plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackAlerts'] --- import stackAlertsObj from './stack_alerts.devdocs.json'; diff --git a/api_docs/task_manager.devdocs.json b/api_docs/task_manager.devdocs.json index 98ff17ac35f74b..0a6584f16c6daf 100644 --- a/api_docs/task_manager.devdocs.json +++ b/api_docs/task_manager.devdocs.json @@ -429,10 +429,10 @@ "interfaces": [ { "parentPluginId": "taskManager", - "id": "def-server.BulkUpdateSchedulesResult", + "id": "def-server.BulkUpdateTaskResult", "type": "Interface", "tags": [], - "label": "BulkUpdateSchedulesResult", + "label": "BulkUpdateTaskResult", "description": [ "\nreturn type of TaskScheduling.bulkUpdateSchedules method" ], @@ -442,7 +442,7 @@ "children": [ { "parentPluginId": "taskManager", - "id": "def-server.BulkUpdateSchedulesResult.tasks", + "id": "def-server.BulkUpdateTaskResult.tasks", "type": "Array", "tags": [], "label": "tasks", @@ -465,7 +465,7 @@ }, { "parentPluginId": "taskManager", - "id": "def-server.BulkUpdateSchedulesResult.errors", + "id": "def-server.BulkUpdateTaskResult.errors", "type": "Array", "tags": [], "label": "errors", @@ -993,6 +993,22 @@ "path": "x-pack/plugins/task_manager/server/task.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "taskManager", + "id": "def-server.TaskInstance.enabled", + "type": "CompoundType", + "tags": [], + "label": "enabled", + "description": [ + "\nIndicates whether the task is currently enabled. Disabled tasks will not be claimed." + ], + "signature": [ + "boolean | undefined" + ], + "path": "x-pack/plugins/task_manager/server/task.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -1216,7 +1232,7 @@ "\nA task instance that has an id and is ready for storage." ], "signature": [ - "{ params: Record; state: Record; scope?: string[] | undefined; taskType: string; }" + "{ params: Record; enabled?: boolean | undefined; state: Record; scope?: string[] | undefined; taskType: string; }" ], "path": "x-pack/plugins/task_manager/server/task.ts", "deprecated": false, @@ -1516,7 +1532,7 @@ "signature": [ "Pick<", "TaskScheduling", - ", \"schedule\" | \"runSoon\" | \"ephemeralRunNow\" | \"ensureScheduled\" | \"bulkUpdateSchedules\" | \"bulkSchedule\"> & Pick<", + ", \"schedule\" | \"runSoon\" | \"ephemeralRunNow\" | \"ensureScheduled\" | \"bulkUpdateSchedules\" | \"bulkEnableDisable\" | \"bulkSchedule\"> & Pick<", "TaskStore", ", \"get\" | \"aggregate\" | \"fetch\" | \"remove\"> & { removeIfExists: (id: string) => Promise; } & { supportsEphemeralTasks: () => boolean; }" ], diff --git a/api_docs/task_manager.mdx b/api_docs/task_manager.mdx index 6f5a5d420ec861..f7980371f9fb5b 100644 --- a/api_docs/task_manager.mdx +++ b/api_docs/task_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/taskManager title: "taskManager" image: https://source.unsplash.com/400x175/?github description: API docs for the taskManager plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'taskManager'] --- import taskManagerObj from './task_manager.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Response Ops](https://github.com/orgs/elastic/teams/response-ops) for q | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 82 | 0 | 41 | 7 | +| 83 | 0 | 41 | 7 | ## Server diff --git a/api_docs/telemetry.mdx b/api_docs/telemetry.mdx index fe725f5cd654d4..d4f289390f0ef6 100644 --- a/api_docs/telemetry.mdx +++ b/api_docs/telemetry.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetry title: "telemetry" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetry plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetry'] --- import telemetryObj from './telemetry.devdocs.json'; diff --git a/api_docs/telemetry_collection_manager.mdx b/api_docs/telemetry_collection_manager.mdx index c510c72159d824..a6f53f88fc967d 100644 --- a/api_docs/telemetry_collection_manager.mdx +++ b/api_docs/telemetry_collection_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionManager title: "telemetryCollectionManager" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryCollectionManager plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionManager'] --- import telemetryCollectionManagerObj from './telemetry_collection_manager.devdocs.json'; diff --git a/api_docs/telemetry_collection_xpack.mdx b/api_docs/telemetry_collection_xpack.mdx index 79bcdf8736b5d0..1f8de356eb2cf5 100644 --- a/api_docs/telemetry_collection_xpack.mdx +++ b/api_docs/telemetry_collection_xpack.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionXpack title: "telemetryCollectionXpack" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryCollectionXpack plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionXpack'] --- import telemetryCollectionXpackObj from './telemetry_collection_xpack.devdocs.json'; diff --git a/api_docs/telemetry_management_section.mdx b/api_docs/telemetry_management_section.mdx index a79d78af7bd71b..504bf789c1623e 100644 --- a/api_docs/telemetry_management_section.mdx +++ b/api_docs/telemetry_management_section.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryManagementSection title: "telemetryManagementSection" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryManagementSection plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryManagementSection'] --- import telemetryManagementSectionObj from './telemetry_management_section.devdocs.json'; diff --git a/api_docs/threat_intelligence.mdx b/api_docs/threat_intelligence.mdx index 5a830485f55ec6..8c7e2dd55ebe77 100644 --- a/api_docs/threat_intelligence.mdx +++ b/api_docs/threat_intelligence.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/threatIntelligence title: "threatIntelligence" image: https://source.unsplash.com/400x175/?github description: API docs for the threatIntelligence plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'threatIntelligence'] --- import threatIntelligenceObj from './threat_intelligence.devdocs.json'; diff --git a/api_docs/timelines.devdocs.json b/api_docs/timelines.devdocs.json index 72a30421386dfc..49bd26f376c08d 100644 --- a/api_docs/timelines.devdocs.json +++ b/api_docs/timelines.devdocs.json @@ -377,7 +377,7 @@ "EsQueryConfig", "; indexPattern: ", "DataViewBase", - "; queries: ", + " | undefined; queries: ", "Query", "[]; filters: ", "Filter", @@ -425,9 +425,8 @@ "label": "indexPattern", "description": [], "signature": [ - "{ fields: ", - "DataViewFieldBase", - "[]; id?: string | undefined; title: string; }" + "DataViewBase", + " | undefined" ], "path": "x-pack/plugins/timelines/public/components/utils/keury/index.ts", "deprecated": false, diff --git a/api_docs/timelines.mdx b/api_docs/timelines.mdx index e8be0345a839ff..1306817e42aaab 100644 --- a/api_docs/timelines.mdx +++ b/api_docs/timelines.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/timelines title: "timelines" image: https://source.unsplash.com/400x175/?github description: API docs for the timelines plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'timelines'] --- import timelinesObj from './timelines.devdocs.json'; diff --git a/api_docs/transform.mdx b/api_docs/transform.mdx index d53e2f51177d78..1769b4f8a2dfcd 100644 --- a/api_docs/transform.mdx +++ b/api_docs/transform.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/transform title: "transform" image: https://source.unsplash.com/400x175/?github description: API docs for the transform plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'transform'] --- import transformObj from './transform.devdocs.json'; diff --git a/api_docs/triggers_actions_ui.devdocs.json b/api_docs/triggers_actions_ui.devdocs.json index 81216ef5e5c188..cdea7649565a3d 100644 --- a/api_docs/triggers_actions_ui.devdocs.json +++ b/api_docs/triggers_actions_ui.devdocs.json @@ -3480,7 +3480,7 @@ "label": "actionVariables", "description": [], "signature": [ - "AsActionVariables<\"params\"> & Partial>" + "AsActionVariables<\"params\"> & Partial>" ], "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", "deprecated": false, @@ -4590,7 +4590,7 @@ "label": "ActionVariables", "description": [], "signature": [ - "AsActionVariables<\"params\"> & Partial>" + "AsActionVariables<\"params\"> & Partial>" ], "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", "deprecated": false, diff --git a/api_docs/triggers_actions_ui.mdx b/api_docs/triggers_actions_ui.mdx index 97a09460258c2e..8fb46329235b40 100644 --- a/api_docs/triggers_actions_ui.mdx +++ b/api_docs/triggers_actions_ui.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/triggersActionsUi title: "triggersActionsUi" image: https://source.unsplash.com/400x175/?github description: API docs for the triggersActionsUi plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'triggersActionsUi'] --- import triggersActionsUiObj from './triggers_actions_ui.devdocs.json'; diff --git a/api_docs/ui_actions.mdx b/api_docs/ui_actions.mdx index e95f3879bfb834..f5639b18111e4c 100644 --- a/api_docs/ui_actions.mdx +++ b/api_docs/ui_actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uiActions title: "uiActions" image: https://source.unsplash.com/400x175/?github description: API docs for the uiActions plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActions'] --- import uiActionsObj from './ui_actions.devdocs.json'; diff --git a/api_docs/ui_actions_enhanced.mdx b/api_docs/ui_actions_enhanced.mdx index 477dcd64c4e87e..aee9a0b2919cc0 100644 --- a/api_docs/ui_actions_enhanced.mdx +++ b/api_docs/ui_actions_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uiActionsEnhanced title: "uiActionsEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the uiActionsEnhanced plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActionsEnhanced'] --- import uiActionsEnhancedObj from './ui_actions_enhanced.devdocs.json'; diff --git a/api_docs/unified_field_list.mdx b/api_docs/unified_field_list.mdx index f2fbbc473c25c0..7b8ad7644e5311 100644 --- a/api_docs/unified_field_list.mdx +++ b/api_docs/unified_field_list.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedFieldList title: "unifiedFieldList" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedFieldList plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedFieldList'] --- import unifiedFieldListObj from './unified_field_list.devdocs.json'; diff --git a/api_docs/unified_search.devdocs.json b/api_docs/unified_search.devdocs.json index 909ff5f2d26f2c..4d5026322ffbb9 100644 --- a/api_docs/unified_search.devdocs.json +++ b/api_docs/unified_search.devdocs.json @@ -875,6 +875,205 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "unifiedSearch", + "id": "def-public.IUnifiedSearchPluginServices", + "type": "Interface", + "tags": [], + "label": "IUnifiedSearchPluginServices", + "description": [], + "signature": [ + { + "pluginId": "unifiedSearch", + "scope": "public", + "docId": "kibUnifiedSearchPluginApi", + "section": "def-public.IUnifiedSearchPluginServices", + "text": "IUnifiedSearchPluginServices" + }, + " extends Partial<", + { + "pluginId": "core", + "scope": "public", + "docId": "kibCorePluginApi", + "section": "def-public.CoreStart", + "text": "CoreStart" + }, + ">" + ], + "path": "src/plugins/unified_search/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedSearch", + "id": "def-public.IUnifiedSearchPluginServices.unifiedSearch", + "type": "Object", + "tags": [], + "label": "unifiedSearch", + "description": [], + "signature": [ + "{ autocomplete: { getQuerySuggestions: ", + { + "pluginId": "unifiedSearch", + "scope": "public", + "docId": "kibUnifiedSearchAutocompletePluginApi", + "section": "def-public.QuerySuggestionGetFn", + "text": "QuerySuggestionGetFn" + }, + "; hasQuerySuggestions: (language: string) => boolean; getValueSuggestions: ", + "ValueSuggestionsGetFn", + "; }; }" + ], + "path": "src/plugins/unified_search/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedSearch", + "id": "def-public.IUnifiedSearchPluginServices.appName", + "type": "string", + "tags": [], + "label": "appName", + "description": [], + "path": "src/plugins/unified_search/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedSearch", + "id": "def-public.IUnifiedSearchPluginServices.uiSettings", + "type": "Object", + "tags": [], + "label": "uiSettings", + "description": [], + "signature": [ + "IUiSettingsClient" + ], + "path": "src/plugins/unified_search/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedSearch", + "id": "def-public.IUnifiedSearchPluginServices.savedObjects", + "type": "Object", + "tags": [], + "label": "savedObjects", + "description": [], + "signature": [ + "SavedObjectsStart" + ], + "path": "src/plugins/unified_search/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedSearch", + "id": "def-public.IUnifiedSearchPluginServices.notifications", + "type": "Object", + "tags": [], + "label": "notifications", + "description": [], + "signature": [ + "NotificationsStart" + ], + "path": "src/plugins/unified_search/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedSearch", + "id": "def-public.IUnifiedSearchPluginServices.application", + "type": "Object", + "tags": [], + "label": "application", + "description": [], + "signature": [ + "ApplicationStart" + ], + "path": "src/plugins/unified_search/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedSearch", + "id": "def-public.IUnifiedSearchPluginServices.http", + "type": "Object", + "tags": [], + "label": "http", + "description": [], + "signature": [ + "HttpSetup" + ], + "path": "src/plugins/unified_search/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedSearch", + "id": "def-public.IUnifiedSearchPluginServices.storage", + "type": "Object", + "tags": [], + "label": "storage", + "description": [], + "signature": [ + { + "pluginId": "kibanaUtils", + "scope": "public", + "docId": "kibKibanaUtilsPluginApi", + "section": "def-public.IStorageWrapper", + "text": "IStorageWrapper" + }, + "" + ], + "path": "src/plugins/unified_search/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedSearch", + "id": "def-public.IUnifiedSearchPluginServices.data", + "type": "Object", + "tags": [], + "label": "data", + "description": [], + "signature": [ + { + "pluginId": "data", + "scope": "public", + "docId": "kibDataPluginApi", + "section": "def-public.DataPublicPluginStart", + "text": "DataPublicPluginStart" + } + ], + "path": "src/plugins/unified_search/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedSearch", + "id": "def-public.IUnifiedSearchPluginServices.usageCollection", + "type": "Object", + "tags": [], + "label": "usageCollection", + "description": [], + "signature": [ + { + "pluginId": "usageCollection", + "scope": "public", + "docId": "kibUsageCollectionPluginApi", + "section": "def-public.UsageCollectionStart", + "text": "UsageCollectionStart" + }, + " | undefined" + ], + "path": "src/plugins/unified_search/public/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "unifiedSearch", "id": "def-public.QueryStringInputProps", @@ -1481,7 +1680,7 @@ "tags": [], "label": "ui", "description": [ - "\nprewired UI components\n{@link DataPublicPluginStartUi}" + "\nprewired UI components\n{@link UnifiedSearchPublicPluginStartUi}" ], "signature": [ "UnifiedSearchPublicPluginStartUi" diff --git a/api_docs/unified_search.mdx b/api_docs/unified_search.mdx index c14315f59ce5ab..eaa5c330c6a37d 100644 --- a/api_docs/unified_search.mdx +++ b/api_docs/unified_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedSearch title: "unifiedSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedSearch plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch'] --- import unifiedSearchObj from './unified_search.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Unified Search](https://github.com/orgs/elastic/teams/kibana-app-servic | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 111 | 2 | 85 | 16 | +| 122 | 2 | 96 | 16 | ## Client diff --git a/api_docs/unified_search_autocomplete.mdx b/api_docs/unified_search_autocomplete.mdx index 74c9ed470c1f10..7e5d6dae5ddec1 100644 --- a/api_docs/unified_search_autocomplete.mdx +++ b/api_docs/unified_search_autocomplete.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedSearch-autocomplete title: "unifiedSearch.autocomplete" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedSearch.autocomplete plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch.autocomplete'] --- import unifiedSearchAutocompleteObj from './unified_search_autocomplete.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Unified Search](https://github.com/orgs/elastic/teams/kibana-app-servic | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 111 | 2 | 85 | 16 | +| 122 | 2 | 96 | 16 | ## Client diff --git a/api_docs/url_forwarding.mdx b/api_docs/url_forwarding.mdx index b9c921a284d1d1..d2adfc684fb7c3 100644 --- a/api_docs/url_forwarding.mdx +++ b/api_docs/url_forwarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/urlForwarding title: "urlForwarding" image: https://source.unsplash.com/400x175/?github description: API docs for the urlForwarding plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'urlForwarding'] --- import urlForwardingObj from './url_forwarding.devdocs.json'; diff --git a/api_docs/usage_collection.devdocs.json b/api_docs/usage_collection.devdocs.json index 091cf692aebb70..7d580cf4289f82 100644 --- a/api_docs/usage_collection.devdocs.json +++ b/api_docs/usage_collection.devdocs.json @@ -1822,17 +1822,6 @@ "description": [ "\nThe attributes stored in the UsageCounters' SavedObjects" ], - "signature": [ - { - "pluginId": "usageCollection", - "scope": "server", - "docId": "kibUsageCollectionPluginApi", - "section": "def-server.UsageCountersSavedObjectAttributes", - "text": "UsageCountersSavedObjectAttributes" - }, - " extends ", - "SavedObjectAttributes" - ], "path": "src/plugins/usage_collection/server/usage_counters/saved_objects.ts", "deprecated": false, "trackAdoption": false, diff --git a/api_docs/usage_collection.mdx b/api_docs/usage_collection.mdx index 4de9f1cf325bfd..3f237f46c69411 100644 --- a/api_docs/usage_collection.mdx +++ b/api_docs/usage_collection.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/usageCollection title: "usageCollection" image: https://source.unsplash.com/400x175/?github description: API docs for the usageCollection plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'usageCollection'] --- import usageCollectionObj from './usage_collection.devdocs.json'; diff --git a/api_docs/ux.mdx b/api_docs/ux.mdx index d497f6bb0a4466..530d6840be3bad 100644 --- a/api_docs/ux.mdx +++ b/api_docs/ux.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ux title: "ux" image: https://source.unsplash.com/400x175/?github description: API docs for the ux plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ux'] --- import uxObj from './ux.devdocs.json'; diff --git a/api_docs/vis_default_editor.mdx b/api_docs/vis_default_editor.mdx index 1237c1960ae3bd..83eec9c086f771 100644 --- a/api_docs/vis_default_editor.mdx +++ b/api_docs/vis_default_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visDefaultEditor title: "visDefaultEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the visDefaultEditor plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visDefaultEditor'] --- import visDefaultEditorObj from './vis_default_editor.devdocs.json'; diff --git a/api_docs/vis_type_gauge.mdx b/api_docs/vis_type_gauge.mdx index 54d7da6ff41a93..51ce7a31dada46 100644 --- a/api_docs/vis_type_gauge.mdx +++ b/api_docs/vis_type_gauge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeGauge title: "visTypeGauge" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeGauge plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeGauge'] --- import visTypeGaugeObj from './vis_type_gauge.devdocs.json'; diff --git a/api_docs/vis_type_heatmap.mdx b/api_docs/vis_type_heatmap.mdx index 48c4485641f9b5..e118434acda273 100644 --- a/api_docs/vis_type_heatmap.mdx +++ b/api_docs/vis_type_heatmap.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeHeatmap title: "visTypeHeatmap" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeHeatmap plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeHeatmap'] --- import visTypeHeatmapObj from './vis_type_heatmap.devdocs.json'; diff --git a/api_docs/vis_type_pie.mdx b/api_docs/vis_type_pie.mdx index f03b8bea14f0b6..62189f36bf5904 100644 --- a/api_docs/vis_type_pie.mdx +++ b/api_docs/vis_type_pie.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypePie title: "visTypePie" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypePie plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypePie'] --- import visTypePieObj from './vis_type_pie.devdocs.json'; diff --git a/api_docs/vis_type_table.mdx b/api_docs/vis_type_table.mdx index db63128f3733ec..38b7f401826003 100644 --- a/api_docs/vis_type_table.mdx +++ b/api_docs/vis_type_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTable title: "visTypeTable" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTable plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTable'] --- import visTypeTableObj from './vis_type_table.devdocs.json'; diff --git a/api_docs/vis_type_timelion.mdx b/api_docs/vis_type_timelion.mdx index 3767eed28f79c3..c9c97d09973e7b 100644 --- a/api_docs/vis_type_timelion.mdx +++ b/api_docs/vis_type_timelion.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTimelion title: "visTypeTimelion" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTimelion plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimelion'] --- import visTypeTimelionObj from './vis_type_timelion.devdocs.json'; diff --git a/api_docs/vis_type_timeseries.mdx b/api_docs/vis_type_timeseries.mdx index eaa8be6fe9eaa5..a3c374fdfc827e 100644 --- a/api_docs/vis_type_timeseries.mdx +++ b/api_docs/vis_type_timeseries.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTimeseries title: "visTypeTimeseries" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTimeseries plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimeseries'] --- import visTypeTimeseriesObj from './vis_type_timeseries.devdocs.json'; diff --git a/api_docs/vis_type_vega.mdx b/api_docs/vis_type_vega.mdx index 56642da0d7bb1d..a8b4c2fd1134dc 100644 --- a/api_docs/vis_type_vega.mdx +++ b/api_docs/vis_type_vega.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeVega title: "visTypeVega" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeVega plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVega'] --- import visTypeVegaObj from './vis_type_vega.devdocs.json'; diff --git a/api_docs/vis_type_vislib.mdx b/api_docs/vis_type_vislib.mdx index 20e284f805f36e..511dafbda6f495 100644 --- a/api_docs/vis_type_vislib.mdx +++ b/api_docs/vis_type_vislib.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeVislib title: "visTypeVislib" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeVislib plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVislib'] --- import visTypeVislibObj from './vis_type_vislib.devdocs.json'; diff --git a/api_docs/vis_type_xy.mdx b/api_docs/vis_type_xy.mdx index d7a9c73ca02656..5cf1e8abda71c7 100644 --- a/api_docs/vis_type_xy.mdx +++ b/api_docs/vis_type_xy.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeXy title: "visTypeXy" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeXy plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeXy'] --- import visTypeXyObj from './vis_type_xy.devdocs.json'; diff --git a/api_docs/visualizations.mdx b/api_docs/visualizations.mdx index d708564591c581..bacaf0f14bc405 100644 --- a/api_docs/visualizations.mdx +++ b/api_docs/visualizations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visualizations title: "visualizations" image: https://source.unsplash.com/400x175/?github description: API docs for the visualizations plugin -date: 2022-09-09 +date: 2022-09-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visualizations'] --- import visualizationsObj from './visualizations.devdocs.json'; diff --git a/dev_docs/operations/operations_landing.mdx b/dev_docs/operations/operations_landing.mdx index 3a80faa90c501f..88e4be9eb93a30 100644 --- a/dev_docs/operations/operations_landing.mdx +++ b/dev_docs/operations/operations_landing.mdx @@ -71,8 +71,6 @@ layout: landing { pageId: "kibDevDocsOpsDevCliRunner" }, { pageId: "kibDevDocsOpsGetRepoFiles" }, { pageId: "kibDevDocsOpsRepoSourceClassifier" }, - { pageId: "kibDevDocsOpsJsonc" }, - { pageId: "kibDevDocsOpsKibanaManifestParser" }, { pageId: "kibDevDocsOpsKibanaManifestSchema" }, { pageId: "kibDevDocsOpsManagedVscodeConfig" }, { pageId: "kibDevDocsOpsManagedVscodeConfigCli" }, diff --git a/dev_docs/tutorials/testing_plugins.mdx b/dev_docs/tutorials/testing_plugins.mdx index 4fbfd1b2574437..84e43310e46c8a 100644 --- a/dev_docs/tutorials/testing_plugins.mdx +++ b/dev_docs/tutorials/testing_plugins.mdx @@ -688,11 +688,10 @@ reflected in the mock. // src/plugins/myplugin/public/saved_query_service.ts import { SavedObjectsClientContract, - SavedObjectAttributes, SimpleSavedObject, } from 'src/core/public'; -export type SavedQueryAttributes = SavedObjectAttributes & { +export type SavedQueryAttributes = { title: string; description: 'bar'; query: { diff --git a/docs/api/actions-and-connectors/create.asciidoc b/docs/api/actions-and-connectors/create.asciidoc index e8533e0561ccee..c33cbdd77232c3 100644 --- a/docs/api/actions-and-connectors/create.asciidoc +++ b/docs/api/actions-and-connectors/create.asciidoc @@ -83,6 +83,41 @@ For more information, refer to <>. For more information, refer to <>. ===== +.{sn-itom}, {sn-itsm}, and {sn-sir} connectors +[%collapsible%open] +===== +`apiUrl`:: +(Required, string) The {sn} instance URL. + +`clientId`:: +(Required^*^, string) The client ID assigned to your OAuth application. This +property is required when `isOAuth` is `true`. + +`isOAuth`:: +(Optional, string) The type of authentication to use. The default value is +`false`, which means basic authentication is used instead of open authorization +(OAuth). + +`jwtKeyId`:: +(Required^*^, string) The key identifier assigned to the JWT verifier map of +your OAuth application. This property is required when `isOAuth` is `true`. + +`userIdentifierValue`:: +(Required^*^, string) The identifier to use for OAuth authentication. This +identifier should be the user field you selected when you created an OAuth +JWT API endpoint for external clients in your {sn} instance. For example, if +the selected user field is `Email`, the user identifier should be the user's +email address. This property is required when `isOAuth` is `true`. + +`usesTableApi`:: +(Optional, string) Determines whether the connector uses the Table API or the +Import Set API. This property is supported only for {sn-itsm} and {sn-sir} +connectors. ++ +NOTE: If this property is set to false, the Elastic application should be +installed in {sn}. +===== + .{swimlane} connectors [%collapsible%open] ===== @@ -373,7 +408,7 @@ For more configuration properties, refer to <>. `connector_type_id`:: (Required, string) The connector type ID for the connector. For example, -`.cases-webhook`, `.index`, `.jira`, or `.server-log`. +`.cases-webhook`, `.index`, `.jira`, `.server-log`, or `.servicenow-itom`. `name`:: (Required, string) The display name for the connector. @@ -412,6 +447,31 @@ authentication. (Required, string) The account email for HTTP Basic authentication. ===== +.{sn-itom}, {sn-itsm}, and {sn-sir} connectors +[%collapsible%open] +===== +`clientSecret`:: +(Required^*^, string) The client secret assigned to your OAuth application. This +property is required when `isOAuth` is `true`. + +`password`:: +(Required^*^, string) The password for HTTP basic authentication. This property +is required when `isOAuth` is `false`. + +`privateKey`:: +(Required^*^, string) The RSA private key that you created for use in {sn}. This +property is required when `isOAuth` is `true`. + +privateKeyPassword:: +(Required^*^, string) The password for the RSA private key. This property is +required when `isOAuth` is `true` and you set a password on your private key. + +`username`:: +(Required^*^, string) The username for HTTP basic authentication. This property +is required when `isOAuth` is `false`. + +===== + .{swimlane} connectors [%collapsible%open] ===== @@ -516,6 +576,29 @@ POST api/actions/connector -------------------------------------------------- // KIBANA +Create an {sn-itom} connector that uses open authorization: + +[source,sh] +-------------------------------------------------- +POST api/actions/connector +{ + "name": "my-itom-connector", + "connector_type_id": ".servicenow-itom", + "config": { + "apiUrl": "https://exmaple.service-now.com/", + "clientId": "abcdefghijklmnopqrstuvwxyzabcdef", + "isOAuth": "true", + "jwtKeyId": "fedcbazyxwvutsrqponmlkjihgfedcba", + "userIdentifierValue": "testuser@email.com" + }, + "secrets": { + "clientSecret": "secretsecret", + "privateKey": "-----BEGIN RSA PRIVATE KEY-----\nprivatekeyhere\n-----END RSA PRIVATE KEY-----" + } +} +-------------------------------------------------- +// KIBANA + Create a {swimlane} connector: [source,sh] diff --git a/docs/api/actions-and-connectors/execute.asciidoc b/docs/api/actions-and-connectors/execute.asciidoc index 29e9bfa4cf224f..e477d7237c2737 100644 --- a/docs/api/actions-and-connectors/execute.asciidoc +++ b/docs/api/actions-and-connectors/execute.asciidoc @@ -50,19 +50,23 @@ depending on the connector type. For information about the parameter properties, refer to <>. + -- -.Index connectors +.`Params` properties [%collapsible%open] ==== + +.Index connectors +[%collapsible%open] +===== `documents`:: (Required, array of objects) The documents to index in JSON format. For more information, refer to {kibana-ref}/index-action-type.html[Index connector and action]. -==== +===== .Jira connectors [%collapsible%open] -==== +===== `subAction`:: (Required, string) The action to test. Valid values include: `fieldsByIssueType`, `getFields`, `getIncident`, `issue`, `issues`, `issueTypes`, and `pushToService`. @@ -74,55 +78,55 @@ on the `subAction` value. This object is not required when `subAction` is + .Properties when `subAction` is `fieldsByIssueType` [%collapsible%open] -===== +====== `id`::: (Required, string) The Jira issue type identifier. For example, `10024`. -===== +====== + .Properties when `subAction` is `getIncident` [%collapsible%open] -===== +====== `externalId`::: (Required, string) The Jira issue identifier. For example, `71778`. -===== +====== + .Properties when `subAction` is `issue` [%collapsible%open] -===== +====== `id`::: (Required, string) The Jira issue identifier. For example, `71778`. -===== +====== + .Properties when `subAction` is `issues` [%collapsible%open] -===== +====== `title`::: (Required, string) The title of the Jira issue. -===== +====== + .Properties when `subAction` is `pushToService` [%collapsible%open] -===== -comments::: +====== +`comments`::: (Optional, array of objects) Additional information that is sent to Jira. + .Properties of `comments` [%collapsible%open] -====== -comment:::: +======= +`comment`:::: (string) A comment related to the incident. For example, describe how to troubleshoot the issue. -commentId:::: +`commentId`:::: (integer) A unique identifier for the comment. -====== +======= -incident::: +`incident`::: (Required, object) Information necessary to create or update a Jira incident. + .Properties of `incident` [%collapsible%open] -====== +======= `description`:::: (Optional, string) The details about the incident. @@ -151,22 +155,288 @@ types of issues. `title`:::: (Optional, string) A title for the incident, used for searching the contents of the knowledge base. +======= ====== -===== For more information, refer to {kibana-ref}/jira-action-type.html[{jira} connector and action]. -==== +===== + +.{sn-itom} connectors +[%collapsible%open] +===== +`subAction`:: +(Required, string) The action to test. Valid values include: `addEvent` and +`getChoices`. + +`subActionParams`:: +(Required^*^, object) The set of configuration properties, which vary depending +on the `subAction` value. ++ +.Properties when `subAction` is `addEvent` +[%collapsible%open] +====== +`additional_info`:::: +(Optional, string) Additional information about the event. + +`description`:::: +(Optional, string) The details about the event. + +`event_class`:::: +(Optional, string) A specific instance of the source. + +`message_key`:::: +(Optional, string) All actions sharing this key are associated with the same +{sn} alert. The default value is `:`. + +`metric_name`:::: +(Optional, string) The name of the metric. + +`node`:::: +(Optional, string) The host that the event was triggered for. + +`resource`:::: +(Optional, string) The name of the resource. + +`severity`:::: +(Optional, string) The severity of the event. + +`source`:::: +(Optional, string) The name of the event source type. + +`time_of_event`:::: +(Optional, string) The time of the event. + +`type`:::: +(Optional, string) The type of event. +====== ++ +.Properties when `subAction` is `getChoices` +[%collapsible%open] +====== +`fields`:::: +(Required, array of strings) An array of fields. For example, `["severity"]`. +====== +===== + +.{sn-itsm} connectors +[%collapsible%open] +===== +`subAction`:: +(Required, string) The action to test. Valid values include: `getFields`, +`getIncident`, `getChoices`, and `pushToService`. + +`subActionParams`:: +(Required^*^, object) The set of configuration properties, which vary depending +on the `subAction` value. This object is not required when `subAction` is +`getFields`. ++ +.Properties when `subAction` is `getChoices` +[%collapsible%open] +====== +`fields`:::: +(Required, array of strings) An array of fields. For example, `["category","impact"]`. +====== ++ +.Properties when `subAction` is `getIncident` +[%collapsible%open] +====== +`externalId`:::: +(Required, string) The {sn-itsm} issue identifier. +====== ++ +.Properties when `subAction` is `pushToService` +[%collapsible%open] +====== +`comments`::: +(Optional, array of objects) Additional information that is sent to {sn-sir}. ++ +.Properties of `comments` +[%collapsible%open] +======= +`comment`:::: +(string) A comment related to the incident. For example, describe how to +troubleshoot the issue. + +`commentId`:::: +(integer) A unique identifier for the comment. + +//// +version:::: +(string) TBD +//// +======= + +`incident`::: +(Required, object) Information necessary to create or update a {sn-sir} incident. ++ +.Properties of `incident` +[%collapsible%open] +======= +`category`:::: +(Optional, string) The category of the incident. + +`correlation_display`:::: +(Optional, string) A descriptive label of the alert for correlation purposes in +{sn}. + +`correlation_id`:::: +(Optional, string) The correlation identifier for the security incident. +Connectors using the same correlation ID are associated with the same {sn} +incident. This value determines whether a new {sn} incident is created or an +existing one is updated. Modifying this value is optional; if not modified, the +rule ID and alert ID are combined as `{{ruleID}}:{{alert ID}}` to form the +correlation ID value in {sn}. The maximum character length for this value is 100 +characters. ++ +NOTE: Using the default configuration of `{{ruleID}}:{{alert ID}}` ensures +that {sn} creates a separate incident record for every generated alert that uses +a unique alert ID. If the rule generates multiple alerts that use the same alert +IDs, {sn} creates and continually updates a single incident record for the alert. + +`description`:::: +(Optional, string) The details about the incident. + +`externalId`:::: +(Optional, string) The {sn-itsm} issue identifier. If present, the incident is +updated. Otherwise, a new incident is created. + +`impact`:::: +(Optional, string) The impact in {sn-itsm}. + +`severity`:::: +(Optional, string) The severity of the incident. + +`short_description`:::: +(Required, string) A short description for the incident, used for searching the +contents of the knowledge base. + +`subcategory`:::: +(Optional, string) The subcategory in {sn-itsm}. + +`urgency`:::: +(Optional, string) The urgency in {sn-itsm}. +======= +====== +===== + +.{sn-sir} connectors +[%collapsible%open] +===== +`subAction`:: +(Required, string) The action to test. Valid values include: `getFields`, +`getIncident`, `getChoices`, and `pushToService`. + +`subActionParams`:: +(Required^*^, object) The set of configuration properties, which vary depending +on the `subAction` value. This object is not required when `subAction` is +`getFields`. ++ +.Properties when `subAction` is `getChoices` +[%collapsible%open] +====== +`fields`:::: +(Required, array of strings) An array of fields. For example, `["priority","category"]`. +====== ++ +.Properties when `subAction` is `getIncident` +[%collapsible%open] +====== +`externalId`:::: +(Required, string) The {sn-sir} issue identifier. +====== ++ +.Properties when `subAction` is `pushToService` +[%collapsible%open] +====== +`comments`::: +(Optional, array of objects) Additional information that is sent to {sn-sir}. ++ +.Properties of `comments` +[%collapsible%open] +======= +`comment`:::: +(string) A comment related to the incident. For example, describe how to +troubleshoot the issue. + +`commentId`:::: +(integer) A unique identifier for the comment. + +//// +`version`:::: +(string) TBD +//// +======= + +`incident`::: +(Required, object) Information necessary to create or update a {sn-sir} incident. ++ +.Properties of `incident` +[%collapsible%open] +======= +`category`:::: +(Optional, string) The category of the incident. + +`correlation_display`:::: +(Optional, string) A descriptive label of the alert for correlation purposes in +{sn}. + +`correlation_id`:::: +(Optional, string) The correlation identifier for the security incident. +Connectors using the same correlation ID are associated with the same {sn} +incident. This value determines whether a new {sn} incident is created or an +existing one is updated. Modifying this value is optional; if not modified, the +rule ID and alert ID are combined as `{{ruleID}}:{{alert ID}}` to form the +correlation ID value in {sn}. The maximum character length for this value is 100 +characters. ++ +NOTE: Using the default configuration of `{{ruleID}}:{{alert ID}}` ensures that +{sn} creates a separate incident record for every generated alert that uses a +unique alert ID. If the rule generates multiple alerts that use the same alert +IDs, {sn} creates and continually updates a single incident record for the alert. + +`description`:::: +(Optional, string) The details about the incident. + +`dest_ip`:::: +(Optional, string or array of strings) A list of destination IP addresses related +to the security incident. The IPs are added as observables to the security incident. + +`externalId`:::: +(Optional, string) The {sn-sir} issue identifier. If present, the incident is +updated. Otherwise, a new incident is created. + +`malware_hash`:::: +(Optional, string or array of strings) A list of malware URLs related to the +security incident. The URLs are added as observables to the security incident. + +`priority`:::: +(Optional, string) The priority of the incident. + +`short_description`:::: +(Required, string) A short description for the incident, used for searching the +contents of the knowledge base. + +`source_ip`:::: +(Optional, string or array of strings) A list of source IP addresses related to +the security incident. The IPs are added as observables to the security incident. + +`subcategory`:::: +(Optional, string) The subcategory of the incident. +======= +====== +===== .Server log connectors [%collapsible%open] -==== +===== `level`:: (Optional, string) The log level of the message: `trace`, `debug`, `info`, `warn`, `error`, or `fatal`. Defaults to `info`. `message`:: (Required, string) The message to log. +===== ==== -- @@ -277,4 +547,40 @@ The API returns the following: ], "connector_id":"b3aad810-edbe-11ec-82d1-11348ecbf4a6" } +-------------------------------------------------- + +Retrieve the list of choices for a {sn-itom} connector: + +[source,sh] +-------------------------------------------------- +POST api/actions/connector/9d9be270-2fd2-11ed-b0e0-87533c532698/_execute +{ + "params": { + "subAction": "getChoices", + "subActionParams": { + "fields": [ "severity","urgency" ] + } + } +} +-------------------------------------------------- +// KIBANA + +The API returns the severity and urgency choices, for example: + +[source,sh] +-------------------------------------------------- +{ + "status": "ok", + "data":[ + {"dependent_value":"","label":"Critical","value":"1","element":"severity"}, + {"dependent_value":"","label":"Major","value":"2","element":"severity"}, + {"dependent_value":"","label":"Minor","value":"3","element":"severity"}, + {"dependent_value":"","label":"Warning","value":"4","element":"severity"}, + {"dependent_value":"","label":"OK","value":"5","element":"severity"}, + {"dependent_value":"","label":"Clear","value":"0","element":"severity"}, + {"dependent_value":"","label":"1 - High","value":"1","element":"urgency"}, + {"dependent_value":"","label":"2 - Medium","value":"2","element":"urgency"}, + {"dependent_value":"","label":"3 - Low","value":"3","element":"urgency"}], + "connector_id":"9d9be270-2fd2-11ed-b0e0-87533c532698" +} -------------------------------------------------- \ No newline at end of file diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 88fc015ac8b9a4..afdfbdfd02eb07 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -106,7 +106,7 @@ capturing the page with a screenshot. As a result, a download will be available, but there will likely be errors in the visualizations in the report. `xpack.screenshotting.capture.loadDelay`:: -deprecated:[8.0.0,This setting has no effect.] Specify the {time-units}[amount of time] before taking a screenshot when visualizations are not evented. All visualizations that ship with {kib} are evented, so this setting should not have much effect. If you are seeing empty images instead of visualizations, try increasing this value. Defaults to `3s`. *NOTE*: This setting exists for backwards compatibility, but is unused and therefore does not have an affect on reporting performance. +deprecated:[8.0.0,This setting has no effect.] Specify the {time-units}[amount of time] before taking a screenshot when visualizations are not evented. All visualizations that ship with {kib} are evented, so this setting should not have much effect. If you are seeing empty images instead of visualizations, try increasing this value. *NOTE*: This setting exists for backwards compatibility, but is unused and therefore does not have an affect on reporting performance. [float] [[reporting-chromium-settings]] diff --git a/examples/embeddable_examples/common/book_saved_object_attributes.ts b/examples/embeddable_examples/common/book_saved_object_attributes.ts index 0ab84e42e3d9f3..bf1073486fcfb0 100644 --- a/examples/embeddable_examples/common/book_saved_object_attributes.ts +++ b/examples/embeddable_examples/common/book_saved_object_attributes.ts @@ -5,12 +5,9 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -import type { SavedObjectAttributes } from '@kbn/core/types'; - export const BOOK_SAVED_OBJECT = 'book'; -export interface BookSavedObjectAttributes extends SavedObjectAttributes { +export interface BookSavedObjectAttributes { title: string; author?: string; readIt?: boolean; diff --git a/examples/embeddable_examples/common/todo_saved_object_attributes.ts b/examples/embeddable_examples/common/todo_saved_object_attributes.ts index 21994add4ed42d..e165522e88590d 100644 --- a/examples/embeddable_examples/common/todo_saved_object_attributes.ts +++ b/examples/embeddable_examples/common/todo_saved_object_attributes.ts @@ -6,9 +6,7 @@ * Side Public License, v 1. */ -import type { SavedObjectAttributes } from '@kbn/core/types'; - -export interface TodoSavedObjectAttributes extends SavedObjectAttributes { +export interface TodoSavedObjectAttributes { task: string; icon?: string; title?: string; diff --git a/examples/search_examples/public/search/app.tsx b/examples/search_examples/public/search/app.tsx index 94cf19436c3f55..01ebd4433af10b 100644 --- a/examples/search_examples/public/search/app.tsx +++ b/examples/search_examples/public/search/app.tsx @@ -107,9 +107,9 @@ export const SearchExamplesApp = ({ const [selectedBucketField, setSelectedBucketField] = useState< DataViewField | null | undefined >(); - const [request, setRequest] = useState>({}); const [isLoading, setIsLoading] = useState(false); const [currentAbortController, setAbortController] = useState(); + const [request, setRequest] = useState>({}); const [rawResponse, setRawResponse] = useState>({}); const [warningContents, setWarningContents] = useState([]); const [selectedTab, setSelectedTab] = useState(0); @@ -202,6 +202,8 @@ export const SearchExamplesApp = ({ // Submit the search request using the `data.search` service. setRequest(req.params.body); + setRawResponse({}); + setWarningContents([]); setIsLoading(true); data.search @@ -301,6 +303,8 @@ export const SearchExamplesApp = ({ searchSource.setField('aggs', ac); } setRequest(searchSource.getSearchRequestBody()); + setRawResponse({}); + setWarningContents([]); const abortController = new AbortController(); const inspector: Required = { diff --git a/kbn_pm/src/cli.mjs b/kbn_pm/src/cli.mjs index 99d8262f92aa9b..2c9d1019b588f5 100644 --- a/kbn_pm/src/cli.mjs +++ b/kbn_pm/src/cli.mjs @@ -18,7 +18,7 @@ import { Args } from './lib/args.mjs'; import { getHelp } from './lib/help.mjs'; import { createFlagError, isCliError } from './lib/cli_error.mjs'; -import { COMMANDS } from './commands/index.mjs'; +import { getCmd } from './commands/index.mjs'; import { Log } from './lib/log.mjs'; const start = Date.now(); @@ -39,7 +39,7 @@ async function tryToGetCiStatsReporter(log) { } try { - const cmd = cmdName ? COMMANDS.find((c) => c.name === cmdName) : undefined; + const cmd = getCmd(cmdName); if (cmdName && !cmd) { throw createFlagError(`Invalid command name [${cmdName}]`); diff --git a/kbn_pm/src/commands/bootstrap/bootstrap_command.mjs b/kbn_pm/src/commands/bootstrap/bootstrap_command.mjs index 328971313039ac..5a2575c6454151 100644 --- a/kbn_pm/src/commands/bootstrap/bootstrap_command.mjs +++ b/kbn_pm/src/commands/bootstrap/bootstrap_command.mjs @@ -76,7 +76,7 @@ export const command = { // That is only intended during the migration process while non Bazel projects are not removed at all. if (forceInstall) { await time('force install dependencies', async () => { - removeYarnIntegrityFileIfExists(); + await removeYarnIntegrityFileIfExists(); await Bazel.expungeCache(log, { quiet }); await Bazel.installYarnDeps(log, { offline, quiet }); }); @@ -89,19 +89,17 @@ export const command = { // generate the synthetic package map which powers several other features, needed // as an input to the package build await time('regenerate synthetic package map', async () => { - regenerateSyntheticPackageMap(plugins); + await regenerateSyntheticPackageMap(plugins); }); - // build packages await time('build packages', async () => { await Bazel.buildPackages(log, { offline, quiet }); }); - await time('sort package json', async () => { await sortPackageJson(); }); await time('regenerate tsconfig.base.json', async () => { - regenerateBaseTsconfig(plugins); + await regenerateBaseTsconfig(plugins); }); if (validate) { diff --git a/kbn_pm/src/commands/bootstrap/regenerate_base_tsconfig.mjs b/kbn_pm/src/commands/bootstrap/regenerate_base_tsconfig.mjs index 69b288f7981bb4..3cf71531614a57 100644 --- a/kbn_pm/src/commands/bootstrap/regenerate_base_tsconfig.mjs +++ b/kbn_pm/src/commands/bootstrap/regenerate_base_tsconfig.mjs @@ -7,7 +7,7 @@ */ import Path from 'path'; -import Fs from 'fs'; +import Fsp from 'fs/promises'; import { REPO_ROOT } from '../../lib/paths.mjs'; import { convertPluginIdToPackageId } from './plugins.mjs'; @@ -16,9 +16,9 @@ import { normalizePath } from './normalize_path.mjs'; /** * @param {import('@kbn/plugin-discovery').KibanaPlatformPlugin[]} plugins */ -export function regenerateBaseTsconfig(plugins) { +export async function regenerateBaseTsconfig(plugins) { const tsconfigPath = Path.resolve(REPO_ROOT, 'tsconfig.base.json'); - const lines = Fs.readFileSync(tsconfigPath, 'utf-8').split('\n'); + const lines = (await Fsp.readFile(tsconfigPath, 'utf-8')).split('\n'); const packageMap = plugins .slice() @@ -32,7 +32,7 @@ export function regenerateBaseTsconfig(plugins) { const start = lines.findIndex((l) => l.trim() === '// START AUTOMATED PACKAGE LISTING'); const end = lines.findIndex((l) => l.trim() === '// END AUTOMATED PACKAGE LISTING'); - Fs.writeFileSync( + await Fsp.writeFile( tsconfigPath, [...lines.slice(0, start + 1), ...packageMap, ...lines.slice(end)].join('\n') ); diff --git a/kbn_pm/src/commands/bootstrap/regenerate_synthetic_package_map.mjs b/kbn_pm/src/commands/bootstrap/regenerate_synthetic_package_map.mjs index 22898daa92b211..ea1f53727997d5 100644 --- a/kbn_pm/src/commands/bootstrap/regenerate_synthetic_package_map.mjs +++ b/kbn_pm/src/commands/bootstrap/regenerate_synthetic_package_map.mjs @@ -7,7 +7,7 @@ */ import Path from 'path'; -import Fs from 'fs'; +import Fsp from 'fs/promises'; import { normalizePath } from './normalize_path.mjs'; import { REPO_ROOT } from '../../lib/paths.mjs'; @@ -16,7 +16,7 @@ import { convertPluginIdToPackageId } from './plugins.mjs'; /** * @param {import('@kbn/plugin-discovery').KibanaPlatformPlugin[]} plugins */ -export function regenerateSyntheticPackageMap(plugins) { +export async function regenerateSyntheticPackageMap(plugins) { /** @type {Array<[string, string]>} */ const entries = [['@kbn/core', 'src/core']]; @@ -27,7 +27,7 @@ export function regenerateSyntheticPackageMap(plugins) { ]); } - Fs.writeFileSync( + await Fsp.writeFile( Path.resolve(REPO_ROOT, 'packages/kbn-synthetic-package-map/synthetic-packages.json'), JSON.stringify(entries, null, 2) ); diff --git a/kbn_pm/src/commands/bootstrap/yarn.mjs b/kbn_pm/src/commands/bootstrap/yarn.mjs index db11c3b12930e6..652c5cdb38b6e5 100644 --- a/kbn_pm/src/commands/bootstrap/yarn.mjs +++ b/kbn_pm/src/commands/bootstrap/yarn.mjs @@ -7,20 +7,20 @@ */ import Path from 'path'; -import Fs from 'fs'; +import Fsp from 'fs/promises'; import { REPO_ROOT } from '../../lib/paths.mjs'; import { maybeRealpath, isFile, isDirectory } from '../../lib/fs.mjs'; // yarn integrity file checker -export function removeYarnIntegrityFileIfExists() { +export async function removeYarnIntegrityFileIfExists() { try { const nodeModulesRealPath = maybeRealpath(Path.resolve(REPO_ROOT, 'node_modules')); const yarnIntegrityFilePath = Path.resolve(nodeModulesRealPath, '.yarn-integrity'); // check if the file exists and delete it in that case if (isFile(yarnIntegrityFilePath)) { - Fs.unlinkSync(yarnIntegrityFilePath); + await Fsp.unlink(yarnIntegrityFilePath); } } catch { // no-op diff --git a/kbn_pm/src/commands/index.mjs b/kbn_pm/src/commands/index.mjs index 8d4638310d3290..f8b63d7afe5fce 100644 --- a/kbn_pm/src/commands/index.mjs +++ b/kbn_pm/src/commands/index.mjs @@ -12,4 +12,12 @@ export const COMMANDS = [ (await import('./run_in_packages_command.mjs')).command, (await import('./clean_command.mjs')).command, (await import('./reset_command.mjs')).command, + (await import('./test_command.mjs')).command, ]; + +/** + * @param {string | undefined} name + */ +export function getCmd(name) { + return COMMANDS.find((c) => (c.name.startsWith('_') ? c.name.slice(1) : c.name) === name); +} diff --git a/test/visual_regression/services/index.ts b/kbn_pm/src/commands/test_command.mjs similarity index 61% rename from test/visual_regression/services/index.ts rename to kbn_pm/src/commands/test_command.mjs index 9aefe1f8de7809..e425c5b94698d7 100644 --- a/test/visual_regression/services/index.ts +++ b/kbn_pm/src/commands/test_command.mjs @@ -6,10 +6,10 @@ * Side Public License, v 1. */ -import { services as functionalServices } from '../../functional/services'; -import { VisualTestingService } from './visual_testing'; - -export const services = { - ...functionalServices, - visualTesting: VisualTestingService, +/** @type {import('../lib/command').Command} */ +export const command = { + name: '_test', + async run({ log }) { + log.success('empty'); + }, }; diff --git a/kbn_pm/src/lib/find_clean_paths.mjs b/kbn_pm/src/lib/find_clean_paths.mjs index e98a949971487f..a15118031038b5 100644 --- a/kbn_pm/src/lib/find_clean_paths.mjs +++ b/kbn_pm/src/lib/find_clean_paths.mjs @@ -30,7 +30,7 @@ async function tryToGetSyntheticPackageMap(log) { } /** - * @param {*} packageDir + * @param {string} packageDir * @returns {string[]} */ export function readCleanPatterns(packageDir) { diff --git a/kbn_pm/src/lib/help.mjs b/kbn_pm/src/lib/help.mjs index bce93df47e9411..28f61bb65c56f5 100644 --- a/kbn_pm/src/lib/help.mjs +++ b/kbn_pm/src/lib/help.mjs @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { COMMANDS } from '../commands/index.mjs'; +import { COMMANDS, getCmd } from '../commands/index.mjs'; import { dedent, indent } from './indent.mjs'; import { title } from './colors.mjs'; @@ -15,7 +15,7 @@ import { title } from './colors.mjs'; * @returns {Promise} */ export async function getHelp(cmdName = undefined) { - const cmd = cmdName && COMMANDS.find((c) => c.name === cmdName); + const cmd = getCmd(cmdName); /** * @param {number} depth @@ -49,6 +49,6 @@ export async function getHelp(cmdName = undefined) { ' yarn kbn [...flags]', '', 'Commands:', - ...COMMANDS.map((cmd) => cmdLines(2, cmd)).flat(), + ...COMMANDS.flatMap((cmd) => (cmd.name.startsWith('_') ? [] : cmdLines(2, cmd))), ].join('\n'); } diff --git a/package.json b/package.json index ae40de1b204f41..4fa23bdb9ef47f 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ }, "dependencies": { "@appland/sql-parser": "^1.5.1", - "@babel/runtime": "^7.18.9", + "@babel/runtime": "^7.19.0", "@dnd-kit/core": "^3.1.1", "@dnd-kit/sortable": "^4.0.0", "@dnd-kit/utilities": "^2.0.0", @@ -156,6 +156,8 @@ "@kbn/core-application-browser-internal": "link:bazel-bin/packages/core/application/core-application-browser-internal", "@kbn/core-application-browser-mocks": "link:bazel-bin/packages/core/application/core-application-browser-mocks", "@kbn/core-application-common": "link:bazel-bin/packages/core/application/core-application-common", + "@kbn/core-apps-browser-internal": "link:bazel-bin/packages/core/apps/core-apps-browser-internal", + "@kbn/core-apps-browser-mocks": "link:bazel-bin/packages/core/apps/core-apps-browser-mocks", "@kbn/core-base-browser-internal": "link:bazel-bin/packages/core/base/core-base-browser-internal", "@kbn/core-base-browser-mocks": "link:bazel-bin/packages/core/base/core-base-browser-mocks", "@kbn/core-base-common": "link:bazel-bin/packages/core/base/core-base-common", @@ -363,7 +365,9 @@ "@kbn/shared-ux-prompt-no-data-views": "link:bazel-bin/packages/shared-ux/prompt/no_data_views/impl", "@kbn/shared-ux-prompt-no-data-views-mocks": "link:bazel-bin/packages/shared-ux/prompt/no_data_views/mocks", "@kbn/shared-ux-prompt-no-data-views-types": "link:bazel-bin/packages/shared-ux/prompt/no_data_views/types", - "@kbn/shared-ux-storybook-config": "link:bazel-bin/packages/shared-ux/storybook/config", + "@kbn/shared-ux-router-mocks": "link:bazel-bin/packages/shared-ux/router/mocks", + "@kbn/shared-ux-services": "link:bazel-bin/packages/kbn-shared-ux-services", + "@kbn/shared-ux-storybook": "link:bazel-bin/packages/kbn-shared-ux-storybook", "@kbn/shared-ux-storybook-mock": "link:bazel-bin/packages/shared-ux/storybook/mock", "@kbn/shared-ux-utility": "link:bazel-bin/packages/kbn-shared-ux-utility", "@kbn/std": "link:bazel-bin/packages/kbn-std", @@ -627,12 +631,12 @@ "devDependencies": { "@apidevtools/swagger-parser": "^10.0.3", "@babel/cli": "^7.18.10", - "@babel/core": "^7.18.13", + "@babel/core": "^7.19.0", "@babel/eslint-parser": "^7.18.9", "@babel/eslint-plugin": "^7.18.10", - "@babel/generator": "^7.18.13", - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/parser": "^7.18.13", + "@babel/generator": "^7.19.0", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/parser": "^7.19.0", "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-export-namespace-from": "^7.18.9", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", @@ -640,17 +644,17 @@ "@babel/plugin-proposal-optional-chaining": "^7.18.9", "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-transform-runtime": "^7.18.10", - "@babel/preset-env": "^7.18.10", + "@babel/preset-env": "^7.19.0", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.18.6", "@babel/register": "^7.18.9", - "@babel/traverse": "^7.18.13", - "@babel/types": "^7.18.13", + "@babel/traverse": "^7.19.0", + "@babel/types": "^7.19.0", "@bazel/ibazel": "^0.16.2", "@bazel/typescript": "4.6.2", - "@cypress/code-coverage": "^3.9.12", + "@cypress/code-coverage": "^3.10.0", "@cypress/snapshot": "^2.1.7", - "@cypress/webpack-preprocessor": "^5.6.0", + "@cypress/webpack-preprocessor": "^5.12.2", "@elastic/eslint-plugin-eui": "0.0.2", "@elastic/github-checks-reporter": "0.0.20b3", "@elastic/makelogs": "^6.0.0", @@ -720,7 +724,6 @@ "@mapbox/vector-tile": "1.3.1", "@octokit/rest": "^16.35.0", "@openpgp/web-stream-tools": "^0.0.10", - "@percy/agent": "^0.28.6", "@storybook/addon-a11y": "^6.4.22", "@storybook/addon-actions": "^6.4.22", "@storybook/addon-controls": "^6.4.22", @@ -843,6 +846,8 @@ "@types/kbn__core-application-browser-internal": "link:bazel-bin/packages/core/application/core-application-browser-internal/npm_module_types", "@types/kbn__core-application-browser-mocks": "link:bazel-bin/packages/core/application/core-application-browser-mocks/npm_module_types", "@types/kbn__core-application-common": "link:bazel-bin/packages/core/application/core-application-common/npm_module_types", + "@types/kbn__core-apps-browser-internal": "link:bazel-bin/packages/core/apps/core-apps-browser-internal/npm_module_types", + "@types/kbn__core-apps-browser-mocks": "link:bazel-bin/packages/core/apps/core-apps-browser-mocks/npm_module_types", "@types/kbn__core-base-browser": "link:bazel-bin/packages/core/base/core-base-browser/npm_module_types", "@types/kbn__core-base-browser-internal": "link:bazel-bin/packages/core/base/core-base-browser-internal/npm_module_types", "@types/kbn__core-base-browser-mocks": "link:bazel-bin/packages/core/base/core-base-browser-mocks/npm_module_types", @@ -1077,7 +1082,9 @@ "@types/kbn__shared-ux-prompt-no-data-views": "link:bazel-bin/packages/shared-ux/prompt/no_data_views/impl/npm_module_types", "@types/kbn__shared-ux-prompt-no-data-views-mocks": "link:bazel-bin/packages/shared-ux/prompt/no_data_views/mocks/npm_module_types", "@types/kbn__shared-ux-prompt-no-data-views-types": "link:bazel-bin/packages/shared-ux/prompt/no_data_views/types/npm_module_types", - "@types/kbn__shared-ux-storybook-config": "link:bazel-bin/packages/shared-ux/storybook/config/npm_module_types", + "@types/kbn__shared-ux-router-mocks": "link:bazel-bin/packages/shared-ux/router/mocks/npm_module_types", + "@types/kbn__shared-ux-services": "link:bazel-bin/packages/kbn-shared-ux-services/npm_module_types", + "@types/kbn__shared-ux-storybook": "link:bazel-bin/packages/kbn-shared-ux-storybook/npm_module_types", "@types/kbn__shared-ux-storybook-mock": "link:bazel-bin/packages/shared-ux/storybook/mock/npm_module_types", "@types/kbn__shared-ux-utility": "link:bazel-bin/packages/kbn-shared-ux-utility/npm_module_types", "@types/kbn__some-dev-log": "link:bazel-bin/packages/kbn-some-dev-log/npm_module_types", @@ -1225,14 +1232,14 @@ "cssnano": "^5.1.12", "cssnano-preset-default": "^5.2.12", "csstype": "^3.0.2", - "cypress": "^9.6.1", - "cypress-axe": "^0.14.0", + "cypress": "^10.7.0", + "cypress-axe": "^1.0.0", "cypress-file-upload": "^5.0.8", - "cypress-multi-reporters": "^1.6.0", + "cypress-multi-reporters": "^1.6.1", "cypress-pipe": "^2.0.0", - "cypress-react-selector": "^2.3.17", - "cypress-real-events": "^1.7.0", - "cypress-recurse": "^1.20.0", + "cypress-react-selector": "^3.0.0", + "cypress-real-events": "^1.7.1", + "cypress-recurse": "^1.23.0", "debug": "^2.6.9", "delete-empty": "^2.0.0", "dependency-check": "^4.1.0", @@ -1313,7 +1320,7 @@ "ms-chromium-edge-driver": "^0.5.1", "mutation-observer": "^1.0.3", "nock": "12.0.3", - "node-sass": "7.0.1", + "node-sass": "^7.0.3", "null-loader": "^3.0.0", "nyc": "^15.1.0", "oboe": "^2.1.4", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index b898faee5fb07e..18c2202fb8d2cd 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -24,6 +24,8 @@ filegroup( "//packages/core/application/core-application-browser-internal:build", "//packages/core/application/core-application-browser-mocks:build", "//packages/core/application/core-application-common:build", + "//packages/core/apps/core-apps-browser-internal:build", + "//packages/core/apps/core-apps-browser-mocks:build", "//packages/core/base/core-base-browser-internal:build", "//packages/core/base/core-base-browser-mocks:build", "//packages/core/base/core-base-common:build", @@ -332,6 +334,8 @@ filegroup( "//packages/core/application/core-application-browser-internal:build_types", "//packages/core/application/core-application-browser-mocks:build_types", "//packages/core/application/core-application-common:build_types", + "//packages/core/apps/core-apps-browser-internal:build_types", + "//packages/core/apps/core-apps-browser-mocks:build_types", "//packages/core/base/core-base-browser-internal:build_types", "//packages/core/base/core-base-browser-mocks:build_types", "//packages/core/base/core-base-common:build_types", diff --git a/packages/core/apps/core-apps-browser-internal/BUILD.bazel b/packages/core/apps/core-apps-browser-internal/BUILD.bazel new file mode 100644 index 00000000000000..4ca5c1a1cf2af1 --- /dev/null +++ b/packages/core/apps/core-apps-browser-internal/BUILD.bazel @@ -0,0 +1,137 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "core-apps-browser-internal" +PKG_REQUIRE_NAME = "@kbn/core-apps-browser-internal" + +SOURCE_FILES = glob( + [ + "**/*.ts", + "**/*.tsx", + ], + exclude = [ + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +RUNTIME_DEPS = [ + "@npm//react", + "@npm//react-dom", + "@npm//history", + "@npm//@elastic/eui", + "//packages/kbn-i18n", + "//packages/kbn-i18n-react", + "//packages/core/mount-utils/core-mount-utils-browser-internal", +] + +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "@npm//@types/react", + "@npm//@types/react-dom", + "@npm//@types/history", + "@npm//@elastic/eui", + "//packages/kbn-i18n:npm_module_types", + "//packages/kbn-i18n-react:npm_module_types", + "//packages/core/base/core-base-browser-internal:npm_module_types", + "//packages/core/injected-metadata/core-injected-metadata-browser-internal:npm_module_types", + "//packages/core/doc-links/core-doc-links-browser:npm_module_types", + "//packages/core/http/core-http-browser:npm_module_types", + "//packages/core/ui-settings/core-ui-settings-browser:npm_module_types", + "//packages/core/notifications/core-notifications-browser:npm_module_types", + "//packages/core/application/core-application-browser:npm_module_types", + "//packages/core/application/core-application-browser-internal:npm_module_types", + "//packages/core/theme/core-theme-browser-internal:npm_module_types", + "//packages/core/mount-utils/core-mount-utils-browser-internal:npm_module_types", + "//packages/core/status/core-status-common-internal:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + declaration_map = True, + emit_declaration_only = True, + out_dir = "target_types", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/core/apps/core-apps-browser-internal/README.md b/packages/core/apps/core-apps-browser-internal/README.md new file mode 100644 index 00000000000000..8412844a911df4 --- /dev/null +++ b/packages/core/apps/core-apps-browser-internal/README.md @@ -0,0 +1,3 @@ +# @kbn/core-apps-browser-internal + +This package contains the internal types and implementation of Core's `coreApps` service. diff --git a/packages/core/apps/core-apps-browser-internal/index.ts b/packages/core/apps/core-apps-browser-internal/index.ts new file mode 100644 index 00000000000000..35678cd2a19fd4 --- /dev/null +++ b/packages/core/apps/core-apps-browser-internal/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ + +export { CoreAppsService, URL_MAX_LENGTH } from './src'; +export type { CoreAppsServiceSetupDeps, CoreAppsServiceStartDeps } from './src'; diff --git a/packages/core/apps/core-apps-browser-internal/jest.config.js b/packages/core/apps/core-apps-browser-internal/jest.config.js new file mode 100644 index 00000000000000..80df7f644eab5f --- /dev/null +++ b/packages/core/apps/core-apps-browser-internal/jest.config.js @@ -0,0 +1,13 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/packages/core/apps/core-apps-browser-internal'], +}; diff --git a/packages/core/apps/core-apps-browser-internal/kibana.jsonc b/packages/core/apps/core-apps-browser-internal/kibana.jsonc new file mode 100644 index 00000000000000..552de143ce1dec --- /dev/null +++ b/packages/core/apps/core-apps-browser-internal/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/core-apps-browser-internal", + "owner": "@elastic/kibana-core", + "runtimeDeps": [], + "typeDeps": [] +} diff --git a/packages/core/apps/core-apps-browser-internal/package.json b/packages/core/apps/core-apps-browser-internal/package.json new file mode 100644 index 00000000000000..58262f9a7aaeb7 --- /dev/null +++ b/packages/core/apps/core-apps-browser-internal/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/core-apps-browser-internal", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "author": "Kibana Core", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/src/core/public/core_app/core_app.ts b/packages/core/apps/core-apps-browser-internal/src/core_app.ts similarity index 91% rename from src/core/public/core_app/core_app.ts rename to packages/core/apps/core-apps-browser-internal/src/core_app.ts index 7b4fd29011b9bd..e8a61de40bea2e 100644 --- a/src/core/public/core_app/core_app.ts +++ b/packages/core/apps/core-apps-browser-internal/src/core_app.ts @@ -25,14 +25,14 @@ import { } from './errors'; import { renderApp as renderStatusApp } from './status'; -export interface SetupDeps { +export interface CoreAppsServiceSetupDeps { application: InternalApplicationSetup; http: HttpSetup; injectedMetadata: InternalInjectedMetadataSetup; notifications: NotificationsSetup; } -export interface StartDeps { +export interface CoreAppsServiceStartDeps { application: InternalApplicationStart; docLinks: DocLinksStart; http: HttpStart; @@ -40,12 +40,12 @@ export interface StartDeps { uiSettings: IUiSettingsClient; } -export class CoreApp { +export class CoreAppsService { private stopHistoryListening?: UnregisterCallback; constructor(private readonly coreContext: CoreContext) {} - public setup({ application, http, injectedMetadata, notifications }: SetupDeps) { + public setup({ application, http, injectedMetadata, notifications }: CoreAppsServiceSetupDeps) { application.register(this.coreContext.coreId, { id: 'error', title: 'App Error', @@ -73,7 +73,13 @@ export class CoreApp { }); } - public start({ application, docLinks, http, notifications, uiSettings }: StartDeps) { + public start({ + application, + docLinks, + http, + notifications, + uiSettings, + }: CoreAppsServiceStartDeps) { if (!application.history) { return; } diff --git a/src/core/public/core_app/errors/error_application.test.ts b/packages/core/apps/core-apps-browser-internal/src/errors/error_application.test.ts similarity index 100% rename from src/core/public/core_app/errors/error_application.test.ts rename to packages/core/apps/core-apps-browser-internal/src/errors/error_application.test.ts diff --git a/src/core/public/core_app/errors/error_application.tsx b/packages/core/apps/core-apps-browser-internal/src/errors/error_application.tsx similarity index 100% rename from src/core/public/core_app/errors/error_application.tsx rename to packages/core/apps/core-apps-browser-internal/src/errors/error_application.tsx diff --git a/src/core/public/core_app/errors/index.ts b/packages/core/apps/core-apps-browser-internal/src/errors/index.ts similarity index 100% rename from src/core/public/core_app/errors/index.ts rename to packages/core/apps/core-apps-browser-internal/src/errors/index.ts diff --git a/src/core/public/core_app/errors/public_base_url.test.tsx b/packages/core/apps/core-apps-browser-internal/src/errors/public_base_url.test.tsx similarity index 100% rename from src/core/public/core_app/errors/public_base_url.test.tsx rename to packages/core/apps/core-apps-browser-internal/src/errors/public_base_url.test.tsx diff --git a/src/core/public/core_app/errors/public_base_url.tsx b/packages/core/apps/core-apps-browser-internal/src/errors/public_base_url.tsx similarity index 95% rename from src/core/public/core_app/errors/public_base_url.tsx rename to packages/core/apps/core-apps-browser-internal/src/errors/public_base_url.tsx index f86903f3c185fe..ec5f45930ce228 100644 --- a/src/core/public/core_app/errors/public_base_url.tsx +++ b/packages/core/apps/core-apps-browser-internal/src/errors/public_base_url.tsx @@ -12,8 +12,9 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import type { DocLinksStart } from '@kbn/core-doc-links-browser'; +import type { HttpStart } from '@kbn/core-http-browser'; +import type { NotificationsStart } from '@kbn/core-notifications-browser'; import { mountReactNode } from '@kbn/core-mount-utils-browser-internal'; -import type { HttpStart, NotificationsStart } from '../..'; /** Only exported for tests */ export const MISSING_CONFIG_STORAGE_KEY = `core.warnings.publicBaseUrlMissingDismissed`; diff --git a/src/core/public/core_app/errors/url_overflow.test.ts b/packages/core/apps/core-apps-browser-internal/src/errors/url_overflow.test.ts similarity index 100% rename from src/core/public/core_app/errors/url_overflow.test.ts rename to packages/core/apps/core-apps-browser-internal/src/errors/url_overflow.test.ts diff --git a/src/core/public/core_app/errors/url_overflow.tsx b/packages/core/apps/core-apps-browser-internal/src/errors/url_overflow.tsx similarity index 100% rename from src/core/public/core_app/errors/url_overflow.tsx rename to packages/core/apps/core-apps-browser-internal/src/errors/url_overflow.tsx diff --git a/src/core/public/core_app/errors/url_overflow_ui.tsx b/packages/core/apps/core-apps-browser-internal/src/errors/url_overflow_ui.tsx similarity index 100% rename from src/core/public/core_app/errors/url_overflow_ui.tsx rename to packages/core/apps/core-apps-browser-internal/src/errors/url_overflow_ui.tsx diff --git a/packages/core/apps/core-apps-browser-internal/src/index.ts b/packages/core/apps/core-apps-browser-internal/src/index.ts new file mode 100644 index 00000000000000..d0bb30144b7dbc --- /dev/null +++ b/packages/core/apps/core-apps-browser-internal/src/index.ts @@ -0,0 +1,11 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +export { CoreAppsService } from './core_app'; +export { URL_MAX_LENGTH } from './errors'; +export type { CoreAppsServiceSetupDeps, CoreAppsServiceStartDeps } from './core_app'; diff --git a/src/core/public/core_app/status/components/__snapshots__/metric_tiles.test.tsx.snap b/packages/core/apps/core-apps-browser-internal/src/status/components/__snapshots__/metric_tiles.test.tsx.snap similarity index 100% rename from src/core/public/core_app/status/components/__snapshots__/metric_tiles.test.tsx.snap rename to packages/core/apps/core-apps-browser-internal/src/status/components/__snapshots__/metric_tiles.test.tsx.snap diff --git a/src/core/public/core_app/status/components/__snapshots__/server_status.test.tsx.snap b/packages/core/apps/core-apps-browser-internal/src/status/components/__snapshots__/server_status.test.tsx.snap similarity index 100% rename from src/core/public/core_app/status/components/__snapshots__/server_status.test.tsx.snap rename to packages/core/apps/core-apps-browser-internal/src/status/components/__snapshots__/server_status.test.tsx.snap diff --git a/src/core/public/core_app/status/components/__snapshots__/status_table.test.tsx.snap b/packages/core/apps/core-apps-browser-internal/src/status/components/__snapshots__/status_table.test.tsx.snap similarity index 100% rename from src/core/public/core_app/status/components/__snapshots__/status_table.test.tsx.snap rename to packages/core/apps/core-apps-browser-internal/src/status/components/__snapshots__/status_table.test.tsx.snap diff --git a/src/core/public/core_app/status/components/index.ts b/packages/core/apps/core-apps-browser-internal/src/status/components/index.ts similarity index 100% rename from src/core/public/core_app/status/components/index.ts rename to packages/core/apps/core-apps-browser-internal/src/status/components/index.ts diff --git a/src/core/public/core_app/status/components/metric_tiles.test.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/metric_tiles.test.tsx similarity index 100% rename from src/core/public/core_app/status/components/metric_tiles.test.tsx rename to packages/core/apps/core-apps-browser-internal/src/status/components/metric_tiles.test.tsx diff --git a/src/core/public/core_app/status/components/metric_tiles.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/metric_tiles.tsx similarity index 100% rename from src/core/public/core_app/status/components/metric_tiles.tsx rename to packages/core/apps/core-apps-browser-internal/src/status/components/metric_tiles.tsx diff --git a/src/core/public/core_app/status/components/server_status.test.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/server_status.test.tsx similarity index 100% rename from src/core/public/core_app/status/components/server_status.test.tsx rename to packages/core/apps/core-apps-browser-internal/src/status/components/server_status.test.tsx diff --git a/src/core/public/core_app/status/components/server_status.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/server_status.tsx similarity index 100% rename from src/core/public/core_app/status/components/server_status.tsx rename to packages/core/apps/core-apps-browser-internal/src/status/components/server_status.tsx diff --git a/src/core/public/core_app/status/components/status_badge.test.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/status_badge.test.tsx similarity index 100% rename from src/core/public/core_app/status/components/status_badge.test.tsx rename to packages/core/apps/core-apps-browser-internal/src/status/components/status_badge.test.tsx diff --git a/src/core/public/core_app/status/components/status_badge.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/status_badge.tsx similarity index 100% rename from src/core/public/core_app/status/components/status_badge.tsx rename to packages/core/apps/core-apps-browser-internal/src/status/components/status_badge.tsx diff --git a/src/core/public/core_app/status/components/status_expanded_row.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/status_expanded_row.tsx similarity index 100% rename from src/core/public/core_app/status/components/status_expanded_row.tsx rename to packages/core/apps/core-apps-browser-internal/src/status/components/status_expanded_row.tsx diff --git a/src/core/public/core_app/status/components/status_section.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/status_section.tsx similarity index 100% rename from src/core/public/core_app/status/components/status_section.tsx rename to packages/core/apps/core-apps-browser-internal/src/status/components/status_section.tsx diff --git a/src/core/public/core_app/status/components/status_table.test.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/status_table.test.tsx similarity index 100% rename from src/core/public/core_app/status/components/status_table.test.tsx rename to packages/core/apps/core-apps-browser-internal/src/status/components/status_table.test.tsx diff --git a/src/core/public/core_app/status/components/status_table.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/status_table.tsx similarity index 100% rename from src/core/public/core_app/status/components/status_table.tsx rename to packages/core/apps/core-apps-browser-internal/src/status/components/status_table.tsx diff --git a/src/core/public/core_app/status/components/version_header.test.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx similarity index 100% rename from src/core/public/core_app/status/components/version_header.test.tsx rename to packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx diff --git a/src/core/public/core_app/status/components/version_header.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.tsx similarity index 100% rename from src/core/public/core_app/status/components/version_header.tsx rename to packages/core/apps/core-apps-browser-internal/src/status/components/version_header.tsx diff --git a/src/core/public/core_app/status/index.ts b/packages/core/apps/core-apps-browser-internal/src/status/index.ts similarity index 100% rename from src/core/public/core_app/status/index.ts rename to packages/core/apps/core-apps-browser-internal/src/status/index.ts diff --git a/src/core/public/core_app/status/lib/format_number.test.ts b/packages/core/apps/core-apps-browser-internal/src/status/lib/format_number.test.ts similarity index 100% rename from src/core/public/core_app/status/lib/format_number.test.ts rename to packages/core/apps/core-apps-browser-internal/src/status/lib/format_number.test.ts diff --git a/src/core/public/core_app/status/lib/format_number.ts b/packages/core/apps/core-apps-browser-internal/src/status/lib/format_number.ts similarity index 100% rename from src/core/public/core_app/status/lib/format_number.ts rename to packages/core/apps/core-apps-browser-internal/src/status/lib/format_number.ts diff --git a/src/core/public/core_app/status/lib/index.ts b/packages/core/apps/core-apps-browser-internal/src/status/lib/index.ts similarity index 100% rename from src/core/public/core_app/status/lib/index.ts rename to packages/core/apps/core-apps-browser-internal/src/status/lib/index.ts diff --git a/src/core/public/core_app/status/lib/load_status.test.ts b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts similarity index 100% rename from src/core/public/core_app/status/lib/load_status.test.ts rename to packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts diff --git a/src/core/public/core_app/status/lib/load_status.ts b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts similarity index 99% rename from src/core/public/core_app/status/lib/load_status.ts rename to packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts index 5a4b2b5907ea2c..c3ebd9923e2a81 100644 --- a/src/core/public/core_app/status/lib/load_status.ts +++ b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts @@ -14,7 +14,7 @@ import type { StatusResponse, StatusInfoServiceStatus as ServiceStatus, } from '@kbn/core-status-common-internal'; -import type { DataType } from '.'; +import type { DataType } from './format_number'; interface MetricMeta { title: string; diff --git a/src/core/public/core_app/status/lib/status_level.test.ts b/packages/core/apps/core-apps-browser-internal/src/status/lib/status_level.test.ts similarity index 100% rename from src/core/public/core_app/status/lib/status_level.test.ts rename to packages/core/apps/core-apps-browser-internal/src/status/lib/status_level.test.ts diff --git a/src/core/public/core_app/status/lib/status_level.ts b/packages/core/apps/core-apps-browser-internal/src/status/lib/status_level.ts similarity index 100% rename from src/core/public/core_app/status/lib/status_level.ts rename to packages/core/apps/core-apps-browser-internal/src/status/lib/status_level.ts diff --git a/src/core/public/core_app/status/render_app.tsx b/packages/core/apps/core-apps-browser-internal/src/status/render_app.tsx similarity index 100% rename from src/core/public/core_app/status/render_app.tsx rename to packages/core/apps/core-apps-browser-internal/src/status/render_app.tsx diff --git a/src/core/public/core_app/status/status_app.tsx b/packages/core/apps/core-apps-browser-internal/src/status/status_app.tsx similarity index 100% rename from src/core/public/core_app/status/status_app.tsx rename to packages/core/apps/core-apps-browser-internal/src/status/status_app.tsx diff --git a/packages/core/apps/core-apps-browser-internal/tsconfig.json b/packages/core/apps/core-apps-browser-internal/tsconfig.json new file mode 100644 index 00000000000000..2249e2ee937617 --- /dev/null +++ b/packages/core/apps/core-apps-browser-internal/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "stripInternal": false, + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ] +} diff --git a/packages/core/apps/core-apps-browser-mocks/BUILD.bazel b/packages/core/apps/core-apps-browser-mocks/BUILD.bazel new file mode 100644 index 00000000000000..42c29b72766b94 --- /dev/null +++ b/packages/core/apps/core-apps-browser-mocks/BUILD.bazel @@ -0,0 +1,115 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "core-apps-browser-mocks" +PKG_REQUIRE_NAME = "@kbn/core-apps-browser-mocks" + +SOURCE_FILES = glob( + [ + "**/*.ts", + "**/*.tsx", + ], + exclude = [ + "**/*.config.js", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +RUNTIME_DEPS = [ + "@npm//react" +] + +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "//packages/kbn-utility-types:npm_module_types", + "//packages/core/apps/core-apps-browser-internal:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + declaration_map = True, + emit_declaration_only = True, + out_dir = "target_types", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/core/apps/core-apps-browser-mocks/README.md b/packages/core/apps/core-apps-browser-mocks/README.md new file mode 100644 index 00000000000000..5ae368e814c435 --- /dev/null +++ b/packages/core/apps/core-apps-browser-mocks/README.md @@ -0,0 +1,4 @@ +# @kbn/core-apps-browser-mocks + +This package contains mocks for Core's browser-side `coreApps` service. +- `coreAppsMock` diff --git a/src/core/public/core_app/index.ts b/packages/core/apps/core-apps-browser-mocks/index.ts similarity index 81% rename from src/core/public/core_app/index.ts rename to packages/core/apps/core-apps-browser-mocks/index.ts index 594f9975d29fb0..68289764ac3e8a 100644 --- a/src/core/public/core_app/index.ts +++ b/packages/core/apps/core-apps-browser-mocks/index.ts @@ -6,5 +6,4 @@ * Side Public License, v 1. */ -export { CoreApp } from './core_app'; -export { URL_MAX_LENGTH } from './errors'; +export { coreAppsMock } from './src'; diff --git a/packages/core/apps/core-apps-browser-mocks/jest.config.js b/packages/core/apps/core-apps-browser-mocks/jest.config.js new file mode 100644 index 00000000000000..f797f04d42061c --- /dev/null +++ b/packages/core/apps/core-apps-browser-mocks/jest.config.js @@ -0,0 +1,13 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/packages/core/apps/core-apps-browser-mocks'], +}; diff --git a/packages/core/apps/core-apps-browser-mocks/kibana.jsonc b/packages/core/apps/core-apps-browser-mocks/kibana.jsonc new file mode 100644 index 00000000000000..074993f2bd62b1 --- /dev/null +++ b/packages/core/apps/core-apps-browser-mocks/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/core-apps-browser-mocks", + "owner": "@elastic/kibana-core", + "runtimeDeps": [], + "typeDeps": [] +} diff --git a/packages/core/apps/core-apps-browser-mocks/package.json b/packages/core/apps/core-apps-browser-mocks/package.json new file mode 100644 index 00000000000000..486f6445a8b248 --- /dev/null +++ b/packages/core/apps/core-apps-browser-mocks/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/core-apps-browser-mocks", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "author": "Kibana Core", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/src/core/public/core_app/core_app.mock.ts b/packages/core/apps/core-apps-browser-mocks/src/core_app.mock.ts similarity index 77% rename from src/core/public/core_app/core_app.mock.ts rename to packages/core/apps/core-apps-browser-mocks/src/core_app.mock.ts index 04a4eefa0d25f3..fb5a3b54ad5916 100644 --- a/src/core/public/core_app/core_app.mock.ts +++ b/packages/core/apps/core-apps-browser-mocks/src/core_app.mock.ts @@ -7,15 +7,16 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { CoreApp } from './core_app'; +import type { CoreAppsService } from '@kbn/core-apps-browser-internal'; + +type CoreAppContract = PublicMethodsOf; -type CoreAppContract = PublicMethodsOf; const createMock = (): jest.Mocked => ({ setup: jest.fn(), start: jest.fn(), stop: jest.fn(), }); -export const coreAppMock = { +export const coreAppsMock = { create: createMock, }; diff --git a/packages/core/apps/core-apps-browser-mocks/src/index.ts b/packages/core/apps/core-apps-browser-mocks/src/index.ts new file mode 100644 index 00000000000000..7afedc3d90fcf2 --- /dev/null +++ b/packages/core/apps/core-apps-browser-mocks/src/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ + +export { coreAppsMock } from './core_app.mock'; diff --git a/packages/core/apps/core-apps-browser-mocks/tsconfig.json b/packages/core/apps/core-apps-browser-mocks/tsconfig.json new file mode 100644 index 00000000000000..26b4c7aca3a676 --- /dev/null +++ b/packages/core/apps/core-apps-browser-mocks/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "stripInternal": false, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ] +} diff --git a/packages/core/saved-objects/core-saved-objects-api-server/src/apis/bulk_update.ts b/packages/core/saved-objects/core-saved-objects-api-server/src/apis/bulk_update.ts index 102e46809b55e6..858504853dd754 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server/src/apis/bulk_update.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server/src/apis/bulk_update.ts @@ -19,7 +19,7 @@ export interface SavedObjectsBulkUpdateObject id: string; /** The type of this Saved Object. Each plugin can define it's own custom Saved Object types. */ type: string; - /** {@inheritdoc SavedObjectAttributes} */ + /** The data for a Saved Object is stored as an object in the `attributes` property. **/ attributes: Partial; /** * Optional namespace string to use when searching for this object. If this is defined, it will supersede the namespace ID that is in diff --git a/packages/core/saved-objects/core-saved-objects-common/src/saved_objects.ts b/packages/core/saved-objects/core-saved-objects-common/src/saved_objects.ts index 2b8f6d66e67ac7..b9bdbafc213c93 100644 --- a/packages/core/saved-objects/core-saved-objects-common/src/saved_objects.ts +++ b/packages/core/saved-objects/core-saved-objects-common/src/saved_objects.ts @@ -31,6 +31,7 @@ export type SavedObjectAttribute = SavedObjectAttributeSingle | SavedObjectAttri * property. * * @public + * @deprecated This type reduces the type safety of your code. Create an interface for your specific saved object type or use `unknown` instead. */ export interface SavedObjectAttributes { [key: string]: SavedObjectAttribute; @@ -76,7 +77,7 @@ export interface SavedObject { /** Timestamp of the last time this document had been updated. */ updated_at?: string; error?: SavedObjectError; - /** {@inheritdoc SavedObjectAttributes} */ + /** The data for a Saved Object is stored as an object in the `attributes` property. **/ attributes: T; /** {@inheritdoc SavedObjectReference} */ references: SavedObjectReference[]; diff --git a/packages/core/saved-objects/core-saved-objects-server/src/contracts.ts b/packages/core/saved-objects/core-saved-objects-server/src/contracts.ts index bb790aec293c82..20821cef46e7a1 100644 --- a/packages/core/saved-objects/core-saved-objects-server/src/contracts.ts +++ b/packages/core/saved-objects/core-saved-objects-server/src/contracts.ts @@ -7,7 +7,6 @@ */ import type { KibanaRequest } from '@kbn/core-http-server'; -import type { SavedObjectAttributes } from '@kbn/core-saved-objects-common'; import type { SavedObjectsClientContract, ISavedObjectsRepository, @@ -124,9 +123,7 @@ export interface SavedObjectsServiceSetup { * } * ``` */ - registerType: ( - type: SavedObjectsType - ) => void; + registerType: (type: SavedObjectsType) => void; /** * Returns the default index used for saved objects. diff --git a/packages/kbn-apm-synthtrace/README.md b/packages/kbn-apm-synthtrace/README.md index dcd50215c6a852..4aaaeee5b672c0 100644 --- a/packages/kbn-apm-synthtrace/README.md +++ b/packages/kbn-apm-synthtrace/README.md @@ -27,7 +27,7 @@ This library can currently be used in two ways: ```ts import { service, timerange, toElasticsearchOutput } from '@kbn/apm-synthtrace'; -const instance = service('synth-go', 'production', 'go').instance('instance-a'); +const instance = service({name: 'synth-go', environment: 'production', agentName: 'go'}).instance('instance-a'); const from = new Date('2021-01-01T12:00:00.000Z').getTime(); const to = new Date('2021-01-01T12:00:00.000Z').getTime(); @@ -37,7 +37,7 @@ const traceEvents = timerange(from, to) .rate(10) .flatMap((timestamp) => instance - .transaction('GET /api/product/list') + .transaction({transactionName: 'GET /api/product/list'}) .timestamp(timestamp) .duration(1000) .success() diff --git a/packages/kbn-apm-synthtrace/src/lib/apm/browser.ts b/packages/kbn-apm-synthtrace/src/lib/apm/browser.ts index ebba6a0e89a696..89a1ac5d34a1d3 100644 --- a/packages/kbn-apm-synthtrace/src/lib/apm/browser.ts +++ b/packages/kbn-apm-synthtrace/src/lib/apm/browser.ts @@ -12,7 +12,13 @@ import { RumSpan } from './rum_span'; import { RumTransaction } from './rum_transaction'; export class Browser extends Entity { - transaction(transactionName: string, transactionType: string = 'page-load') { + transaction({ + transactionName, + transactionType = 'page-load', + }: { + transactionName: string; + transactionType?: string; + }) { return new RumTransaction({ ...this.fields, 'transaction.name': transactionName, @@ -20,7 +26,15 @@ export class Browser extends Entity { }); } - span(spanName: string, spanType: string, spanSubtype: string) { + span({ + spanName, + spanType, + spanSubtype, + }: { + spanName: string; + spanType: string; + spanSubtype: string; + }) { return new RumSpan({ ...this.fields, 'span.name': spanName, @@ -30,11 +44,19 @@ export class Browser extends Entity { } } -export function browser(serviceName: string, production: string, userAgent: ApmUserAgentFields) { +export function browser({ + serviceName, + environment, + userAgent, +}: { + serviceName: string; + environment: string; + userAgent: ApmUserAgentFields; +}) { return new Browser({ 'agent.name': 'rum-js', 'service.name': serviceName, - 'service.environment': production, + 'service.environment': environment, ...userAgent, }); } diff --git a/packages/kbn-apm-synthtrace/src/lib/apm/instance.ts b/packages/kbn-apm-synthtrace/src/lib/apm/instance.ts index d212c1f2cead0b..32a81de9f307a2 100644 --- a/packages/kbn-apm-synthtrace/src/lib/apm/instance.ts +++ b/packages/kbn-apm-synthtrace/src/lib/apm/instance.ts @@ -14,7 +14,13 @@ import { Transaction } from './transaction'; import { ApmApplicationMetricFields, ApmFields } from './apm_fields'; export class Instance extends Entity { - transaction(transactionName: string, transactionType = 'request') { + transaction({ + transactionName, + transactionType = 'request', + }: { + transactionName: string; + transactionType?: string; + }) { return new Transaction({ ...this.fields, 'transaction.name': transactionName, @@ -22,7 +28,16 @@ export class Instance extends Entity { }); } - span(spanName: string, spanType: string, spanSubtype?: string, apmFields?: ApmFields) { + span({ + spanName, + spanType, + spanSubtype, + ...apmFields + }: { + spanName: string; + spanType: string; + spanSubtype?: string; + } & ApmFields) { return new Span({ ...this.fields, ...apmFields, @@ -32,7 +47,15 @@ export class Instance extends Entity { }); } - error(message: string, type?: string, groupingName?: string) { + error({ + message, + type, + groupingName, + }: { + message: string; + type?: string; + groupingName?: string; + }) { return new ApmError({ ...this.fields, 'error.exception': [{ message, ...(type ? { type } : {}) }], diff --git a/packages/kbn-apm-synthtrace/src/lib/apm/service.ts b/packages/kbn-apm-synthtrace/src/lib/apm/service.ts index ded149f0b6236e..0939535a87135e 100644 --- a/packages/kbn-apm-synthtrace/src/lib/apm/service.ts +++ b/packages/kbn-apm-synthtrace/src/lib/apm/service.ts @@ -20,7 +20,15 @@ export class Service extends Entity { } } -export function service(name: string, environment: string, agentName: string) { +export function service({ + name, + environment, + agentName, +}: { + name: string; + environment: string; + agentName: string; +}) { return new Service({ 'service.name': name, 'service.environment': environment, diff --git a/packages/kbn-apm-synthtrace/src/lib/apm/span.ts b/packages/kbn-apm-synthtrace/src/lib/apm/span.ts index 388e65385e7dd8..a5795ae321478e 100644 --- a/packages/kbn-apm-synthtrace/src/lib/apm/span.ts +++ b/packages/kbn-apm-synthtrace/src/lib/apm/span.ts @@ -47,7 +47,7 @@ export function httpExitSpan({ }: { spanName: string; destinationUrl: string; -}): [string, string, string, ApmFields] { +}) { // origin: 'http://opbeans-go:3000', // host: 'opbeans-go:3000', // hostname: 'opbeans-go', @@ -57,39 +57,29 @@ export function httpExitSpan({ const spanType = 'external'; const spanSubType = 'http'; - return [ + return { spanName, spanType, spanSubType, - { - 'destination.address': destination.hostname, - 'destination.port': parseInt(destination.port, 10), - 'service.target.name': destination.host, - 'span.destination.service.name': destination.origin, - 'span.destination.service.resource': destination.host, - 'span.destination.service.type': 'external', - }, - ]; + 'destination.address': destination.hostname, + 'destination.port': parseInt(destination.port, 10), + 'service.target.name': destination.host, + 'span.destination.service.name': destination.origin, + 'span.destination.service.resource': destination.host, + 'span.destination.service.type': 'external', + }; } -export function dbExitSpan({ - spanName, - spanSubType, -}: { - spanName: string; - spanSubType?: string; -}): [string, string, string | undefined, ApmFields] { +export function dbExitSpan({ spanName, spanSubType }: { spanName: string; spanSubType?: string }) { const spanType = 'db'; - return [ + return { spanName, spanType, spanSubType, - { - 'service.target.type': spanSubType, - 'span.destination.service.name': spanSubType, - 'span.destination.service.resource': spanSubType, - 'span.destination.service.type': spanType, - }, - ]; + 'service.target.type': spanSubType, + 'span.destination.service.name': spanSubType, + 'span.destination.service.resource': spanSubType, + 'span.destination.service.type': spanType, + }; } diff --git a/packages/kbn-apm-synthtrace/src/scenarios/aws_lambda.ts b/packages/kbn-apm-synthtrace/src/scenarios/aws_lambda.ts index fa04d6e4f64651..2dcce23ab2a20b 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/aws_lambda.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/aws_lambda.ts @@ -23,7 +23,9 @@ const scenario: Scenario = async (runOptions: RunOptions) => { const range = timerange(from, to); const timestamps = range.ratePerMinute(180); - const instance = apm.service('lambda-python', ENVIRONMENT, 'python').instance('instance'); + const instance = apm + .service({ name: 'lambda-python', environment: ENVIRONMENT, agentName: 'python' }) + .instance('instance'); const traceEventsSetups = [ { functionName: 'lambda-python-1', coldStart: true }, @@ -33,7 +35,7 @@ const scenario: Scenario = async (runOptions: RunOptions) => { const traceEvents = ({ functionName, coldStart }: typeof traceEventsSetups[0]) => { return timestamps.generator((timestamp) => instance - .transaction('GET /order/{id}') + .transaction({ transactionName: 'GET /order/{id}' }) .defaults({ 'service.runtime.name': 'AWS_Lambda_python3.8', 'cloud.provider': 'aws', diff --git a/packages/kbn-apm-synthtrace/src/scenarios/distributed_trace.ts b/packages/kbn-apm-synthtrace/src/scenarios/distributed_trace.ts index a87cbfe5ab4d3d..7834011afa69ae 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/distributed_trace.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/distributed_trace.ts @@ -23,23 +23,27 @@ const scenario: Scenario = async (runOptions: RunOptions) => { const transactionName = '240rpm/75% 1000ms'; const successfulTimestamps = range.interval('1s').rate(3); - const opbeansRum = apm.service('opbeans-rum', ENVIRONMENT, 'rum-js').instance('my-instance'); + const opbeansRum = apm + .service({ name: 'opbeans-rum', environment: ENVIRONMENT, agentName: 'rum-js' }) + .instance('my-instance'); const opbeansNode = apm - .service('opbeans-node', ENVIRONMENT, 'nodejs') + .service({ name: 'opbeans-node', environment: ENVIRONMENT, agentName: 'nodejs' }) + .instance('my-instance'); + const opbeansGo = apm + .service({ name: 'opbeans-go', environment: ENVIRONMENT, agentName: 'go' }) .instance('my-instance'); - const opbeansGo = apm.service('opbeans-go', ENVIRONMENT, 'go').instance('my-instance'); const traces = successfulTimestamps.generator((timestamp) => { // opbeans-rum return opbeansRum - .transaction(transactionName) + .transaction({ transactionName }) .duration(400) .timestamp(timestamp) .children( // opbeans-rum -> opbeans-node opbeansRum .span( - ...httpExitSpan({ + httpExitSpan({ spanName: 'GET /api/products/top', destinationUrl: 'http://opbeans-node:3000', }) @@ -50,14 +54,14 @@ const scenario: Scenario = async (runOptions: RunOptions) => { .children( // opbeans-node opbeansNode - .transaction('Initial transaction in opbeans-node') + .transaction({ transactionName: 'Initial transaction in opbeans-node' }) .duration(300) .timestamp(timestamp) .children( opbeansNode // opbeans-node -> opbeans-go .span( - ...httpExitSpan({ + httpExitSpan({ spanName: 'GET opbeans-go:3000', destinationUrl: 'http://opbeans-go:3000', }) @@ -69,12 +73,12 @@ const scenario: Scenario = async (runOptions: RunOptions) => { // opbeans-go opbeansGo - .transaction('Initial transaction in opbeans-go') + .transaction({ transactionName: 'Initial transaction in opbeans-go' }) .timestamp(timestamp) .duration(200) .children( opbeansGo - .span('custom_operation', 'custom') + .span({ spanName: 'custom_operation', spanType: 'custom' }) .timestamp(timestamp) .duration(100) .success() diff --git a/packages/kbn-apm-synthtrace/src/scenarios/high_throughput.ts b/packages/kbn-apm-synthtrace/src/scenarios/high_throughput.ts index 41b21df2e83e11..1b2ec3d64b0871 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/high_throughput.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/high_throughput.ts @@ -28,11 +28,11 @@ const scenario: Scenario = async (runOptions: RunOptions) => { const instances = services.map((service, index) => apm - .service( - `${service}-${languages[index % languages.length]}`, - 'production', - languages[index % languages.length] - ) + .service({ + name: `${service}-${languages[index % languages.length]}`, + environment: 'production', + agentName: languages[index % languages.length], + }) .instance(`instance-${index}`) ); const entities = [ @@ -68,18 +68,22 @@ const scenario: Scenario = async (runOptions: RunOptions) => { const generateError = index % random(mod, 9) === 0; const generateChildError = index % random(mod, 9) === 0; const span = instance - .transaction(url) + .transaction({ transactionName: url }) .timestamp(timestamp) .duration(duration) .children( instance - .span('GET apm-*/_search', 'db', 'elasticsearch') + .span({ + spanName: 'GET apm-*/_search', + spanType: 'db', + spanSubtype: 'elasticsearch', + }) .duration(childDuration) .destination('elasticsearch') .timestamp(timestamp) .outcome(generateError && generateChildError ? 'failure' : 'success'), instance - .span('custom_operation', 'custom') + .span({ spanName: 'custom_operation', spanType: 'custom' }) .duration(remainderDuration) .success() .timestamp(timestamp + childDuration) @@ -88,7 +92,9 @@ const scenario: Scenario = async (runOptions: RunOptions) => { ? span.success() : span .failure() - .errors(instance.error(`No handler for ${url}`).timestamp(timestamp + 50)); + .errors( + instance.error({ message: `No handler for ${url}` }).timestamp(timestamp + 50) + ); }); return successfulTraceEvents; diff --git a/packages/kbn-apm-synthtrace/src/scenarios/low_throughput.ts b/packages/kbn-apm-synthtrace/src/scenarios/low_throughput.ts index d842a0650b423d..006a5074f7c1bd 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/low_throughput.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/low_throughput.ts @@ -32,11 +32,13 @@ const scenario: Scenario = async (runOptions: RunOptions) => { const instances = services.map((service, index) => apm - .service( - `${services[index % services.length]}-${languages[index % languages.length]}-${index}`, - ENVIRONMENT, - languages[index % languages.length] - ) + .service({ + name: `${services[index % services.length]}-${ + languages[index % languages.length] + }-${index}`, + environment: ENVIRONMENT, + agentName: languages[index % languages.length], + }) .instance(`instance-${index}`) ); @@ -53,18 +55,22 @@ const scenario: Scenario = async (runOptions: RunOptions) => { const generateError = index % random(mod, 9) === 0; const generateChildError = index % random(mod, 9) === 0; const span = instance - .transaction(url) + .transaction({ transactionName: url }) .timestamp(timestamp) .duration(duration) .children( instance - .span('GET apm-*/_search', 'db', 'elasticsearch') + .span({ + spanName: 'GET apm-*/_search', + spanType: 'db', + spanSubtype: 'elasticsearch', + }) .duration(childDuration) .destination('elasticsearch') .timestamp(timestamp) .outcome(generateError && generateChildError ? 'failure' : 'success'), instance - .span('custom_operation', 'custom') + .span({ spanName: 'custom_operation', spanType: 'custom' }) .duration(remainderDuration) .success() .timestamp(timestamp + childDuration) @@ -73,7 +79,9 @@ const scenario: Scenario = async (runOptions: RunOptions) => { ? span.success() : span .failure() - .errors(instance.error(`No handler for ${url}`).timestamp(timestamp + 50)); + .errors( + instance.error({ message: `No handler for ${url}` }).timestamp(timestamp + 50) + ); }); return successfulTraceEvents; diff --git a/packages/kbn-apm-synthtrace/src/scenarios/many_services.ts b/packages/kbn-apm-synthtrace/src/scenarios/many_services.ts index 501d0e678f0f42..1829d46e172324 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/many_services.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/many_services.ts @@ -33,11 +33,13 @@ const scenario: Scenario = async (runOptions: RunOptions) => { const instances = [...Array(numServices).keys()].map((index) => apm - .service( - `${services[index % services.length]}-${languages[index % languages.length]}-${index}`, - ENVIRONMENT, - languages[index % languages.length] - ) + .service({ + name: `${services[index % services.length]}-${ + languages[index % languages.length] + }-${index}`, + environment: ENVIRONMENT, + agentName: languages[index % languages.length], + }) .instance(`instance-${index}`) ); @@ -53,18 +55,22 @@ const scenario: Scenario = async (runOptions: RunOptions) => { const generateError = random(1, 4) % 3 === 0; const generateChildError = random(0, 5) % 2 === 0; const span = instance - .transaction(url) + .transaction({ transactionName: url }) .timestamp(timestamp) .duration(duration) .children( instance - .span('GET apm-*/_search', 'db', 'elasticsearch') + .span({ + spanName: 'GET apm-*/_search', + spanType: 'db', + spanSubtype: 'elasticsearch', + }) .duration(childDuration) .destination('elasticsearch') .timestamp(timestamp) .outcome(generateError && generateChildError ? 'failure' : 'success'), instance - .span('custom_operation', 'custom') + .span({ spanName: 'custom_operation', spanType: 'custom' }) .duration(remainderDuration) .success() .timestamp(timestamp + childDuration) @@ -73,7 +79,9 @@ const scenario: Scenario = async (runOptions: RunOptions) => { ? span.success() : span .failure() - .errors(instance.error(`No handler for ${url}`).timestamp(timestamp + 50)); + .errors( + instance.error({ message: `No handler for ${url}` }).timestamp(timestamp + 50) + ); }); return successfulTraceEvents; diff --git a/packages/kbn-apm-synthtrace/src/scenarios/simple_trace.ts b/packages/kbn-apm-synthtrace/src/scenarios/simple_trace.ts index f8444ab6e58790..8f3c84564787cf 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/simple_trace.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/simple_trace.ts @@ -31,24 +31,30 @@ const scenario: Scenario = async (runOptions: RunOptions) => { const failedTimestamps = range.ratePerMinute(180); const instances = [...Array(numServices).keys()].map((index) => - apm.service(`opbeans-go-${index}`, ENVIRONMENT, 'go').instance('instance') + apm + .service({ name: `opbeans-go-${index}`, environment: ENVIRONMENT, agentName: 'go' }) + .instance('instance') ); const instanceSpans = (instance: Instance) => { const successfulTraceEvents = successfulTimestamps.generator((timestamp) => instance - .transaction(transactionName) + .transaction({ transactionName }) .timestamp(timestamp) .duration(1000) .success() .children( instance - .span('GET apm-*/_search', 'db', 'elasticsearch') + .span({ + spanName: 'GET apm-*/_search', + spanType: 'db', + spanSubtype: 'elasticsearch', + }) .duration(1000) .success() .destination('elasticsearch') .timestamp(timestamp), instance - .span('custom_operation', 'custom') + .span({ spanName: 'custom_operation', spanType: 'custom' }) .duration(100) .success() .timestamp(timestamp) @@ -57,12 +63,14 @@ const scenario: Scenario = async (runOptions: RunOptions) => { const failedTraceEvents = failedTimestamps.generator((timestamp) => instance - .transaction(transactionName) + .transaction({ transactionName }) .timestamp(timestamp) .duration(1000) .failure() .errors( - instance.error('[ResponseError] index_not_found_exception').timestamp(timestamp + 50) + instance + .error({ message: '[ResponseError] index_not_found_exception' }) + .timestamp(timestamp + 50) ) ); diff --git a/packages/kbn-apm-synthtrace/src/scenarios/span_links.ts b/packages/kbn-apm-synthtrace/src/scenarios/span_links.ts index 0c9250dcd16f7b..4267b6465b179f 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/span_links.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/span_links.ts @@ -35,7 +35,7 @@ const scenario: Scenario = async () => { generate: ({ from, to }) => { const producerInternalOnlyInstance = apm - .service('producer-internal-only', ENVIRONMENT, 'go') + .service({ name: 'producer-internal-only', environment: ENVIRONMENT, agentName: 'go' }) .instance('instance-a'); const producerInternalOnlyEvents = timerange( new Date('2022-04-25T19:00:00.000Z'), @@ -45,13 +45,13 @@ const scenario: Scenario = async () => { .rate(1) .generator((timestamp) => { return producerInternalOnlyInstance - .transaction('Transaction A') + .transaction({ transactionName: 'Transaction A' }) .timestamp(timestamp) .duration(1000) .success() .children( producerInternalOnlyInstance - .span('Span A', 'custom') + .span({ spanName: 'Span A', spanType: 'custom' }) .timestamp(timestamp + 50) .duration(100) .success() @@ -62,20 +62,20 @@ const scenario: Scenario = async () => { const spanASpanLink = getSpanLinksFromEvents(producerInternalOnlyApmFields); const producerConsumerInstance = apm - .service('producer-consumer', ENVIRONMENT, 'java') + .service({ name: 'producer-consumer', environment: ENVIRONMENT, agentName: 'java' }) .instance('instance-b'); const producerConsumerEvents = timerange(from, to) .interval('1m') .rate(1) .generator((timestamp) => { return producerConsumerInstance - .transaction('Transaction B') + .transaction({ transactionName: 'Transaction B' }) .timestamp(timestamp) .duration(1000) .success() .children( producerConsumerInstance - .span('Span B', 'external') + .span({ spanName: 'Span B', spanType: 'external' }) .defaults({ 'span.links': shuffle([...generateExternalSpanLinks(), ...spanASpanLink]), }) @@ -88,19 +88,21 @@ const scenario: Scenario = async () => { const producerConsumerApmFields = producerConsumerEvents.toArray(); const spanBSpanLink = getSpanLinksFromEvents(producerConsumerApmFields); - const consumerInstance = apm.service('consumer', ENVIRONMENT, 'ruby').instance('instance-c'); + const consumerInstance = apm + .service({ name: 'consumer', environment: ENVIRONMENT, agentName: 'ruby' }) + .instance('instance-c'); const consumerEvents = timerange(from, to) .interval('1m') .rate(1) .generator((timestamp) => { return consumerInstance - .transaction('Transaction C') + .transaction({ transactionName: 'Transaction C' }) .timestamp(timestamp) .duration(1000) .success() .children( consumerInstance - .span('Span C', 'external') + .span({ spanName: 'Span C', spanType: 'external' }) .defaults({ 'span.links': spanBSpanLink }) .timestamp(timestamp + 50) .duration(900) diff --git a/packages/kbn-apm-synthtrace/src/test/event_dsl_behavior.test.ts b/packages/kbn-apm-synthtrace/src/test/event_dsl_behavior.test.ts index 31a5ed02429d56..3cf1d8500e12d0 100644 --- a/packages/kbn-apm-synthtrace/src/test/event_dsl_behavior.test.ts +++ b/packages/kbn-apm-synthtrace/src/test/event_dsl_behavior.test.ts @@ -19,7 +19,11 @@ describe('DSL invocations', () => { new Date('2021-01-01T00:00:00.000Z'), new Date('2021-01-01T00:15:00.000Z') ); - const javaService = apm.service('opbeans-java', 'production', 'java'); + const javaService = apm.service({ + name: 'opbeans-java', + environment: 'production', + agentName: 'java', + }); const javaInstance = javaService.instance('instance-1'); let globalSeq = 0; @@ -28,13 +32,13 @@ describe('DSL invocations', () => { .rate(1) .generator((timestamp, index) => javaInstance - .transaction(`GET /api/product/${index}/${globalSeq++}`) + .transaction({ transactionName: `GET /api/product/${index}/${globalSeq++}` }) .duration(1000) .success() .timestamp(timestamp) .children( javaInstance - .span('GET apm-*/_search', 'db', 'elasticsearch') + .span({ spanName: 'GET apm-*/_search', spanType: 'db', spanSubtype: 'elasticsearch' }) .success() .duration(900) .timestamp(timestamp + 50) diff --git a/packages/kbn-apm-synthtrace/src/test/rate_per_minute.test.ts b/packages/kbn-apm-synthtrace/src/test/rate_per_minute.test.ts index e40848ab9f47b0..5a100aa8a404d2 100644 --- a/packages/kbn-apm-synthtrace/src/test/rate_per_minute.test.ts +++ b/packages/kbn-apm-synthtrace/src/test/rate_per_minute.test.ts @@ -19,7 +19,11 @@ describe('rate per minute calculations', () => { let events: Array>; beforeEach(() => { - const javaService = apm.service('opbeans-java', 'production', 'java'); + const javaService = apm.service({ + name: 'opbeans-java', + environment: 'production', + agentName: 'java', + }); const javaInstance = javaService.instance('instance-1'); iterable = range @@ -27,13 +31,13 @@ describe('rate per minute calculations', () => { .rate(1) .generator((timestamp) => javaInstance - .transaction('GET /api/product/list') + .transaction({ transactionName: 'GET /api/product/list' }) .duration(1000) .success() .timestamp(timestamp) .children( javaInstance - .span('GET apm-*/_search', 'db', 'elasticsearch') + .span({ spanName: 'GET apm-*/_search', spanType: 'db', spanSubtype: 'elasticsearch' }) .success() .duration(900) .timestamp(timestamp + 50) diff --git a/packages/kbn-apm-synthtrace/src/test/scenarios/01_simple_trace.test.ts b/packages/kbn-apm-synthtrace/src/test/scenarios/01_simple_trace.test.ts index 8f405700fa2c1e..a278997ecdf731 100644 --- a/packages/kbn-apm-synthtrace/src/test/scenarios/01_simple_trace.test.ts +++ b/packages/kbn-apm-synthtrace/src/test/scenarios/01_simple_trace.test.ts @@ -16,7 +16,11 @@ describe('simple trace', () => { let events: Array>; beforeEach(() => { - const javaService = apm.service('opbeans-java', 'production', 'java'); + const javaService = apm.service({ + name: 'opbeans-java', + environment: 'production', + agentName: 'java', + }); const javaInstance = javaService.instance('instance-1').containerId('instance-1'); const range = timerange( @@ -29,13 +33,13 @@ describe('simple trace', () => { .rate(1) .generator((timestamp) => javaInstance - .transaction('GET /api/product/list') + .transaction({ transactionName: 'GET /api/product/list' }) .duration(1000) .success() .timestamp(timestamp) .children( javaInstance - .span('GET apm-*/_search', 'db', 'elasticsearch') + .span({ spanName: 'GET apm-*/_search', spanType: 'db', spanSubtype: 'elasticsearch' }) .success() .duration(900) .timestamp(timestamp + 50) diff --git a/packages/kbn-apm-synthtrace/src/test/scenarios/02_transaction_metrics.test.ts b/packages/kbn-apm-synthtrace/src/test/scenarios/02_transaction_metrics.test.ts index d7f60b2396cd0d..99715ab6998d62 100644 --- a/packages/kbn-apm-synthtrace/src/test/scenarios/02_transaction_metrics.test.ts +++ b/packages/kbn-apm-synthtrace/src/test/scenarios/02_transaction_metrics.test.ts @@ -16,7 +16,11 @@ describe('transaction metrics', () => { let events: Array>; beforeEach(() => { - const javaService = apm.service('opbeans-java', 'production', 'java'); + const javaService = apm.service({ + name: 'opbeans-java', + environment: 'production', + agentName: 'java', + }); const javaInstance = javaService.instance('instance-1'); const range = timerange( @@ -25,7 +29,10 @@ describe('transaction metrics', () => { ); const span = (timestamp: number) => - javaInstance.transaction('GET /api/product/list').duration(1000).timestamp(timestamp); + javaInstance + .transaction({ transactionName: 'GET /api/product/list' }) + .duration(1000) + .timestamp(timestamp); const processor = new StreamProcessor({ processors: [getTransactionMetrics], diff --git a/packages/kbn-apm-synthtrace/src/test/scenarios/03_span_destination_metrics.test.ts b/packages/kbn-apm-synthtrace/src/test/scenarios/03_span_destination_metrics.test.ts index 39c33e6486f692..f5c721221c3283 100644 --- a/packages/kbn-apm-synthtrace/src/test/scenarios/03_span_destination_metrics.test.ts +++ b/packages/kbn-apm-synthtrace/src/test/scenarios/03_span_destination_metrics.test.ts @@ -16,7 +16,11 @@ describe('span destination metrics', () => { let events: Array>; beforeEach(() => { - const javaService = apm.service('opbeans-java', 'production', 'java'); + const javaService = apm.service({ + name: 'opbeans-java', + environment: 'production', + agentName: 'java', + }); const javaInstance = javaService.instance('instance-1'); const range = timerange( @@ -31,13 +35,17 @@ describe('span destination metrics', () => { .rate(25) .generator((timestamp) => javaInstance - .transaction('GET /api/product/list') + .transaction({ transactionName: 'GET /api/product/list' }) .duration(1000) .success() .timestamp(timestamp) .children( javaInstance - .span('GET apm-*/_search', 'db', 'elasticsearch') + .span({ + spanName: 'GET apm-*/_search', + spanType: 'db', + spanSubtype: 'elasticsearch', + }) .timestamp(timestamp) .duration(1000) .destination('elasticsearch') @@ -49,19 +57,23 @@ describe('span destination metrics', () => { .rate(50) .generator((timestamp) => javaInstance - .transaction('GET /api/product/list') + .transaction({ transactionName: 'GET /api/product/list' }) .duration(1000) .failure() .timestamp(timestamp) .children( javaInstance - .span('GET apm-*/_search', 'db', 'elasticsearch') + .span({ + spanName: 'GET apm-*/_search', + spanType: 'db', + spanSubtype: 'elasticsearch', + }) .timestamp(timestamp) .duration(1000) .destination('elasticsearch') .failure(), javaInstance - .span('custom_operation', 'app') + .span({ spanName: 'custom_operation', spanType: 'app' }) .timestamp(timestamp) .duration(500) .success() diff --git a/packages/kbn-apm-synthtrace/src/test/scenarios/04_breakdown_metrics.test.ts b/packages/kbn-apm-synthtrace/src/test/scenarios/04_breakdown_metrics.test.ts index 831ca790e57a65..731dea453058da 100644 --- a/packages/kbn-apm-synthtrace/src/test/scenarios/04_breakdown_metrics.test.ts +++ b/packages/kbn-apm-synthtrace/src/test/scenarios/04_breakdown_metrics.test.ts @@ -22,7 +22,11 @@ describe('breakdown metrics', () => { const INTERVALS = 6; beforeEach(() => { - const javaService = apm.service('opbeans-java', 'production', 'java'); + const javaService = apm.service({ + name: 'opbeans-java', + environment: 'production', + agentName: 'java', + }); const javaInstance = javaService.instance('instance-1'); const start = new Date('2021-01-01T00:00:00.000Z'); @@ -34,15 +38,18 @@ describe('breakdown metrics', () => { .rate(LIST_RATE) .generator((timestamp) => javaInstance - .transaction('GET /api/product/list') + .transaction({ transactionName: 'GET /api/product/list' }) .timestamp(timestamp) .duration(1000) .children( javaInstance - .span('GET apm-*/_search', 'db', 'elasticsearch') + .span({ spanName: 'GET apm-*/_search', spanType: 'db', spanSubtype: 'elasticsearch' }) .timestamp(timestamp + 150) .duration(500), - javaInstance.span('GET foo', 'db', 'redis').timestamp(timestamp).duration(100) + javaInstance + .span({ spanName: 'GET foo', spanType: 'db', spanSubtype: 'redis' }) + .timestamp(timestamp) + .duration(100) ) ); @@ -51,17 +58,17 @@ describe('breakdown metrics', () => { .rate(ID_RATE) .generator((timestamp) => javaInstance - .transaction('GET /api/product/:id') + .transaction({ transactionName: 'GET /api/product/:id' }) .timestamp(timestamp) .duration(1000) .children( javaInstance - .span('GET apm-*/_search', 'db', 'elasticsearch') + .span({ spanName: 'GET apm-*/_search', spanType: 'db', spanSubtype: 'elasticsearch' }) .duration(500) .timestamp(timestamp + 100) .children( javaInstance - .span('bar', 'external', 'http') + .span({ spanName: 'bar', spanType: 'external', spanSubtype: 'http' }) .timestamp(timestamp + 200) .duration(100) ) diff --git a/packages/kbn-apm-synthtrace/src/test/scenarios/05_transactions_with_errors.test.ts b/packages/kbn-apm-synthtrace/src/test/scenarios/05_transactions_with_errors.test.ts index b9b12aeab0754f..305c3ed2d88a4d 100644 --- a/packages/kbn-apm-synthtrace/src/test/scenarios/05_transactions_with_errors.test.ts +++ b/packages/kbn-apm-synthtrace/src/test/scenarios/05_transactions_with_errors.test.ts @@ -14,13 +14,15 @@ describe('transactions with errors', () => { const timestamp = new Date('2021-01-01T00:00:00.000Z').getTime(); beforeEach(() => { - instance = apm.service('opbeans-java', 'production', 'java').instance('instance'); + instance = apm + .service({ name: 'opbeans-java', environment: 'production', agentName: 'java' }) + .instance('instance'); }); it('generates error events', () => { const events = instance - .transaction('GET /api') + .transaction({ transactionName: 'GET /api' }) .timestamp(timestamp) - .errors(instance.error('test error').timestamp(timestamp)) + .errors(instance.error({ message: 'test error' }).timestamp(timestamp)) .serialize(); const errorEvents = events.filter((event) => event['processor.event'] === 'error'); @@ -39,9 +41,9 @@ describe('transactions with errors', () => { it('sets the transaction and trace id', () => { const [transaction, error] = instance - .transaction('GET /api') + .transaction({ transactionName: 'GET /api' }) .timestamp(timestamp) - .errors(instance.error('test error').timestamp(timestamp)) + .errors(instance.error({ message: 'test error' }).timestamp(timestamp)) .serialize(); const keys = ['transaction.id', 'trace.id', 'transaction.type']; @@ -55,9 +57,9 @@ describe('transactions with errors', () => { it('sets the error grouping key', () => { const [, error] = instance - .transaction('GET /api') + .transaction({ transactionName: 'GET /api' }) .timestamp(timestamp) - .errors(instance.error('test error').timestamp(timestamp)) + .errors(instance.error({ message: 'test error' }).timestamp(timestamp)) .serialize(); expect(error['error.grouping_name']).toEqual('test error'); diff --git a/packages/kbn-apm-synthtrace/src/test/scenarios/06_application_metrics.test.ts b/packages/kbn-apm-synthtrace/src/test/scenarios/06_application_metrics.test.ts index 7bae1e51f1ab3a..c9f33c2f237118 100644 --- a/packages/kbn-apm-synthtrace/src/test/scenarios/06_application_metrics.test.ts +++ b/packages/kbn-apm-synthtrace/src/test/scenarios/06_application_metrics.test.ts @@ -14,7 +14,9 @@ describe('application metrics', () => { const timestamp = new Date('2021-01-01T00:00:00.000Z').getTime(); beforeEach(() => { - instance = apm.service('opbeans-java', 'production', 'java').instance('instance'); + instance = apm + .service({ name: 'opbeans-java', environment: 'production', agentName: 'java' }) + .instance('instance'); }); it('generates application metricsets', () => { const events = instance diff --git a/packages/kbn-dev-proc-runner/src/proc.ts b/packages/kbn-dev-proc-runner/src/proc.ts index ffe7cb64641239..d30a893ae4c759 100644 --- a/packages/kbn-dev-proc-runner/src/proc.ts +++ b/packages/kbn-dev-proc-runner/src/proc.ts @@ -6,8 +6,10 @@ * Side Public License, v 1. */ -import { statSync } from 'fs'; +import Fs from 'fs'; +import Path from 'path'; import { promisify } from 'util'; +import stripAnsi from 'strip-ansi'; import execa from 'execa'; import * as Rx from 'rxjs'; @@ -29,6 +31,7 @@ export interface ProcOptions { cwd: string; env?: Record; stdin?: string; + writeLogsToPath?: string; } async function withTimeout( @@ -44,13 +47,21 @@ export type Proc = ReturnType; export function startProc(name: string, options: ProcOptions, log: ToolingLog) { const { cmd, args, cwd, env, stdin } = options; - log.info('[%s] > %s', name, cmd === process.execPath ? 'node' : cmd, args.join(' ')); + let stdioTarget: undefined | NodeJS.WritableStream; + if (!options.writeLogsToPath) { + log.info('starting [%s] > %s', name, cmd === process.execPath ? 'node' : cmd, args.join(' ')); + } else { + stdioTarget = Fs.createWriteStream(options.writeLogsToPath, 'utf8'); + const exec = cmd === process.execPath ? 'node' : cmd; + const relOut = Path.relative(process.cwd(), options.writeLogsToPath); + log.info(`starting [${name}] and writing output to ${relOut} > ${exec} ${args.join(' ')}`); + } // spawn fails with ENOENT when either the // cmd or cwd don't exist, so we check for the cwd // ahead of time so that the error is less ambiguous try { - if (!statSync(cwd).isDirectory()) { + if (!Fs.statSync(cwd).isDirectory()) { throw new Error(`cwd "${cwd}" exists but is not a directory`); } } catch (err) { @@ -104,7 +115,20 @@ export function startProc(name: string, options: ProcOptions, log: ToolingLog) { observeLines(childProcess.stdout!), // TypeScript note: As long as the proc stdio[1] is 'pipe', then stdout will not be null observeLines(childProcess.stderr!) // TypeScript note: As long as the proc stdio[1] is 'pipe', then stderr will not be null ).pipe( - tap((line) => log.write(` ${chalk.gray('proc')} [${chalk.gray(name)}] ${line}`)), + tap({ + next(line) { + if (stdioTarget) { + stdioTarget.write(stripAnsi(line) + '\n'); + } else { + log.write(` ${chalk.gray('proc')} [${chalk.gray(name)}] ${line}`); + } + }, + complete() { + if (stdioTarget) { + stdioTarget.end(); + } + }, + }), share() ); diff --git a/packages/kbn-dev-proc-runner/src/proc_runner.ts b/packages/kbn-dev-proc-runner/src/proc_runner.ts index 56a6ee48c31506..1226cbeb3eef18 100644 --- a/packages/kbn-dev-proc-runner/src/proc_runner.ts +++ b/packages/kbn-dev-proc-runner/src/proc_runner.ts @@ -36,12 +36,12 @@ export class ProcRunner { private procs: Proc[] = []; private signalUnsubscribe: () => void; - constructor(private log: ToolingLog) { + constructor(private readonly log: ToolingLog) { this.log = log.withType('ProcRunner'); this.signalUnsubscribe = exitHook(() => { this.teardown().catch((error) => { - log.error(`ProcRunner teardown error: ${error.stack}`); + this.log.error(`ProcRunner teardown error: ${error.stack}`); }); }); } @@ -58,6 +58,7 @@ export class ProcRunner { waitTimeout = 15 * MINUTE, env = process.env, onEarlyExit, + writeLogsToPath, } = options; const cmd = options.cmd === 'node' ? process.execPath : options.cmd; @@ -79,6 +80,7 @@ export class ProcRunner { cwd, env, stdin, + writeLogsToPath, }); if (onEarlyExit) { diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 7b7c57ff4f7f25..413e8905bea93b 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -360,6 +360,10 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { macos_system_ext: `${SECURITY_SOLUTION_DOCS}deploy-elastic-endpoint.html#system-extension-endpoint`, linux_deadlock: `${SECURITY_SOLUTION_DOCS}ts-management.html#linux-deadlock`, }, + packageActionTroubleshooting: { + // TODO: Pending to be updated when docs are ready + es_connection: '', + }, responseActions: `${SECURITY_SOLUTION_DOCS}response-actions.html`, }, query: { diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index fb934f4aa4f2ab..1ee0d5414b2753 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -264,6 +264,9 @@ export interface DocLinks { macos_system_ext: string; linux_deadlock: string; }; + readonly packageActionTroubleshooting: { + es_connection: string; + }; readonly threatIntelInt: string; readonly responseActions: string; }; diff --git a/packages/kbn-es-query/src/es_query/build_es_query.ts b/packages/kbn-es-query/src/es_query/build_es_query.ts index 1a0ac9cef15eef..497d38a0530cf0 100644 --- a/packages/kbn-es-query/src/es_query/build_es_query.ts +++ b/packages/kbn-es-query/src/es_query/build_es_query.ts @@ -45,7 +45,7 @@ function removeMatchAll(filters: T[]) { * @public */ export function buildEsQuery( - indexPattern: DataViewBase | undefined, + indexPattern: DataViewBase | DataViewBase[] | undefined, queries: AnyQuery | AnyQuery[], filters: Filter | Filter[], config: EsQueryConfig = { @@ -60,7 +60,7 @@ export function buildEsQuery( const validQueries = queries.filter(isOfQueryType).filter((query) => has(query, 'query')); const queriesByLanguage = groupBy(validQueries, 'language'); const kueryQuery = buildQueryFromKuery( - indexPattern, + Array.isArray(indexPattern) ? indexPattern[0] : indexPattern, queriesByLanguage.kuery, { allowLeadingWildcards: config.allowLeadingWildcards }, { diff --git a/packages/kbn-es-query/src/es_query/from_filters.test.ts b/packages/kbn-es-query/src/es_query/from_filters.test.ts index 78b719ccc0e625..d17b0de3768b12 100644 --- a/packages/kbn-es-query/src/es_query/from_filters.test.ts +++ b/packages/kbn-es-query/src/es_query/from_filters.test.ts @@ -15,6 +15,7 @@ describe('build query', () => { const indexPattern: DataViewBase = { fields, title: 'dataView', + id: '1', }; describe('buildQueryFromFilters', () => { @@ -201,5 +202,43 @@ describe('build query', () => { const result = buildQueryFromFilters(filters, indexPattern, { nestedIgnoreUnmapped: true }); expect(result.filter).toEqual(expectedESQueries); }); + + test('should work with multiple data views', () => { + const indexPattern2: DataViewBase = { + fields, + title: 'dataView', + id: '2', + }; + + const filters = [ + { + query: { query_string: { query: 'foo' } }, + meta: { index: '1' }, + }, + { + query: { query_string: { query: 'bar' } }, + meta: { index: '2' }, + }, + ] as Filter[]; + + const result = buildQueryFromFilters(filters, [indexPattern, indexPattern2], { + ignoreFilterIfFieldNotInIndex: false, + }); + + expect(result.filter).toMatchInlineSnapshot(` + Array [ + Object { + "query_string": Object { + "query": "foo", + }, + }, + Object { + "query_string": Object { + "query": "bar", + }, + }, + ] + `); + }); }); }); diff --git a/packages/kbn-es-query/src/es_query/from_filters.ts b/packages/kbn-es-query/src/es_query/from_filters.ts index 871ff77026b54e..2200648a52c4f8 100644 --- a/packages/kbn-es-query/src/es_query/from_filters.ts +++ b/packages/kbn-es-query/src/es_query/from_filters.ts @@ -64,27 +64,34 @@ export interface EsQueryFiltersConfig { * @public */ export const buildQueryFromFilters = ( - filters: Filter[] = [], - indexPattern: DataViewBase | undefined, + inputFilters: Filter[] = [], + inputDataViews: DataViewBase | DataViewBase[] | undefined, { ignoreFilterIfFieldNotInIndex = false, nestedIgnoreUnmapped }: EsQueryFiltersConfig = { ignoreFilterIfFieldNotInIndex: false, } ): BoolQuery => { - filters = filters.filter((filter) => filter && !isFilterDisabled(filter)); + const filters = inputFilters.filter((filter) => filter && !isFilterDisabled(filter)); + const indexPatterns = Array.isArray(inputDataViews) ? inputDataViews : [inputDataViews]; + + const findIndexPattern = (id: string | undefined) => { + return indexPatterns.find((index) => index?.id === id) || indexPatterns[0]; + }; const filtersToESQueries = (negate: boolean) => { return filters .filter((f) => !!f) .filter(filterNegate(negate)) - .filter( - (filter) => !ignoreFilterIfFieldNotInIndex || filterMatchesIndex(filter, indexPattern) - ) + .filter((filter) => { + const indexPattern = findIndexPattern(filter.meta?.index); + return !ignoreFilterIfFieldNotInIndex || filterMatchesIndex(filter, indexPattern); + }) .map((filter) => { - return migrateFilter(filter, indexPattern); + const indexPattern = findIndexPattern(filter.meta?.index); + const migratedFilter = migrateFilter(filter, indexPattern); + return handleNestedFilter(migratedFilter, indexPattern, { + ignoreUnmapped: nestedIgnoreUnmapped, + }); }) - .map((filter) => - handleNestedFilter(filter, indexPattern, { ignoreUnmapped: nestedIgnoreUnmapped }) - ) .map(cleanFilter) .map(translateToQuery); }; diff --git a/packages/kbn-es-query/src/filters/build_filters/get_filter_field.ts b/packages/kbn-es-query/src/filters/build_filters/get_filter_field.ts index 9ae820cfea4e7a..d8ef7e1106c0ba 100644 --- a/packages/kbn-es-query/src/filters/build_filters/get_filter_field.ts +++ b/packages/kbn-es-query/src/filters/build_filters/get_filter_field.ts @@ -8,8 +8,8 @@ import { getExistsFilterField, isExistsFilter } from './exists_filter'; import { getPhrasesFilterField, isPhrasesFilter } from './phrases_filter'; -import { getPhraseFilterField, isPhraseFilter } from './phrase_filter'; -import { getRangeFilterField, isRangeFilter } from './range_filter'; +import { getPhraseFilterField, isPhraseFilter, isScriptedPhraseFilter } from './phrase_filter'; +import { getRangeFilterField, isRangeFilter, isScriptedRangeFilter } from './range_filter'; import type { Filter } from './types'; /** @internal */ @@ -17,13 +17,13 @@ export const getFilterField = (filter: Filter) => { if (isExistsFilter(filter)) { return getExistsFilterField(filter); } - if (isPhraseFilter(filter)) { + if (isPhraseFilter(filter) || isScriptedPhraseFilter(filter)) { return getPhraseFilterField(filter); } if (isPhrasesFilter(filter)) { return getPhrasesFilterField(filter); } - if (isRangeFilter(filter)) { + if (isRangeFilter(filter) || isScriptedRangeFilter(filter)) { return getRangeFilterField(filter); } diff --git a/packages/kbn-es-query/src/filters/build_filters/phrase_filter.test.ts b/packages/kbn-es-query/src/filters/build_filters/phrase_filter.test.ts index 7c7f7dd28f6cad..1088ba196840c2 100644 --- a/packages/kbn-es-query/src/filters/build_filters/phrase_filter.test.ts +++ b/packages/kbn-es-query/src/filters/build_filters/phrase_filter.test.ts @@ -186,4 +186,14 @@ describe('isScriptedPhraseFilter', () => { expect(isScriptedPhraseFilter(filter)).toBe(true); expect(isPhraseFilter(unknownFilter)).toBe(false); }); + + it('should return false if the filter is a range filter', () => { + const filter: Filter = set({ meta: {} }, 'query.script.script.params', { + gt: 0, + lt: 100, + value: 100, + }) as Filter; + + expect(isScriptedPhraseFilter(filter)).toBe(false); + }); }); diff --git a/packages/kbn-es-query/src/filters/build_filters/phrase_filter.ts b/packages/kbn-es-query/src/filters/build_filters/phrase_filter.ts index 752dc8b338661e..3bbf94cd0722a2 100644 --- a/packages/kbn-es-query/src/filters/build_filters/phrase_filter.ts +++ b/packages/kbn-es-query/src/filters/build_filters/phrase_filter.ts @@ -10,6 +10,7 @@ import { get, has, isPlainObject } from 'lodash'; import type { Filter, FilterMeta } from './types'; import type { DataViewFieldBase, DataViewBase } from '../../es_query'; import { getConvertedValueForField } from './get_converted_value_for_field'; +import { hasRangeKeys } from './range_filter'; export type PhraseFilterValue = string | number | boolean; @@ -60,10 +61,12 @@ export const isPhraseFilter = (filter: Filter): filter is PhraseFilter => { * @public */ export const isScriptedPhraseFilter = (filter: Filter): filter is ScriptedPhraseFilter => - has(filter, 'query.script.script.params.value'); + has(filter, 'query.script.script.params.value') && + !hasRangeKeys(filter.query?.script?.script?.params); /** @internal */ -export const getPhraseFilterField = (filter: PhraseFilter) => { +export const getPhraseFilterField = (filter: PhraseFilter | ScriptedPhraseFilter) => { + if (filter.meta?.field) return filter.meta.field; const queryConfig = filter.query.match_phrase ?? filter.query.match ?? {}; return Object.keys(queryConfig)[0]; }; diff --git a/packages/kbn-es-query/src/filters/build_filters/range_filter.ts b/packages/kbn-es-query/src/filters/build_filters/range_filter.ts index 2ff43a854da23a..e9bafade964b71 100644 --- a/packages/kbn-es-query/src/filters/build_filters/range_filter.ts +++ b/packages/kbn-es-query/src/filters/build_filters/range_filter.ts @@ -47,7 +47,7 @@ export interface RangeFilterParams { format?: string; } -const hasRangeKeys = (params: RangeFilterParams) => +export const hasRangeKeys = (params: RangeFilterParams) => Boolean( keys(params).find((key: string) => ['gte', 'gt', 'lte', 'lt', 'from', 'to'].includes(key)) ); @@ -108,8 +108,8 @@ export const isScriptedRangeFilter = (filter: Filter): filter is ScriptedRangeFi /** * @internal */ -export const getRangeFilterField = (filter: RangeFilter) => { - return filter.query.range && Object.keys(filter.query.range)[0]; +export const getRangeFilterField = (filter: RangeFilter | ScriptedRangeFilter) => { + return filter.meta?.field ?? (filter.query.range && Object.keys(filter.query.range)[0]); }; const formatValue = (params: any[]) => diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index 5c410523d70ca6..a027db201b002c 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -6,10 +6,12 @@ * Side Public License, v 1. */ +const fs = require('fs'); const fsp = require('fs/promises'); const execa = require('execa'); const chalk = require('chalk'); const path = require('path'); +const Rx = require('rxjs'); const { Client } = require('@elastic/elasticsearch'); const { downloadSnapshot, installSnapshot, installSource, installArchive } = require('./install'); const { ES_BIN, ES_PLUGIN_BIN, ES_KEYSTORE_BIN } = require('./paths'); @@ -315,6 +317,7 @@ exports.Cluster = class Cluster { startTime, skipReadyCheck, readyTimeout, + writeLogsToPath, ...options } = opts; @@ -322,7 +325,19 @@ exports.Cluster = class Cluster { throw new Error('ES has already been started'); } - this._log.info(chalk.bold('Starting')); + /** @type {NodeJS.WritableStream | undefined} */ + let stdioTarget; + + if (writeLogsToPath) { + stdioTarget = fs.createWriteStream(writeLogsToPath, 'utf8'); + this._log.info( + chalk.bold('Starting'), + `and writing logs to ${path.relative(process.cwd(), writeLogsToPath)}` + ); + } else { + this._log.info(chalk.bold('Starting')); + } + this._log.indent(4); const esArgs = new Map([ @@ -428,7 +443,8 @@ exports.Cluster = class Cluster { let reportSent = false; // parse and forward es stdout to the log this._process.stdout.on('data', (data) => { - const lines = parseEsLog(data.toString()); + const chunk = data.toString(); + const lines = parseEsLog(chunk); lines.forEach((line) => { if (!reportSent && line.message.includes('publish_address')) { reportSent = true; @@ -436,12 +452,36 @@ exports.Cluster = class Cluster { success: true, }); } - this._log.info(line.formattedMessage); + + if (stdioTarget) { + stdioTarget.write(chunk); + } else { + this._log.info(line.formattedMessage); + } }); }); // forward es stderr to the log - this._process.stderr.on('data', (data) => this._log.error(chalk.red(data.toString()))); + this._process.stderr.on('data', (data) => { + const chunk = data.toString(); + if (stdioTarget) { + stdioTarget.write(chunk); + } else { + this._log.error(chalk.red()); + } + }); + + // close the stdio target if we have one defined + if (stdioTarget) { + Rx.combineLatest([ + Rx.fromEvent(this._process.stderr, 'end'), + Rx.fromEvent(this._process.stdout, 'end'), + ]) + .pipe(Rx.first()) + .subscribe(() => { + stdioTarget.end(); + }); + } // observe the exit code of the process and reflect in _outcome promies const exitCode = new Promise((resolve) => this._process.once('exit', resolve)); diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/core/public/core_app/styles/_globals_v7dark.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/core/public/styles/core_app/_globals_v7dark.scss similarity index 100% rename from packages/kbn-optimizer/src/__fixtures__/mock_repo/src/core/public/core_app/styles/_globals_v7dark.scss rename to packages/kbn-optimizer/src/__fixtures__/mock_repo/src/core/public/styles/core_app/_globals_v7dark.scss diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/core/public/core_app/styles/_globals_v7light.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/core/public/styles/core_app/_globals_v7light.scss similarity index 100% rename from packages/kbn-optimizer/src/__fixtures__/mock_repo/src/core/public/core_app/styles/_globals_v7light.scss rename to packages/kbn-optimizer/src/__fixtures__/mock_repo/src/core/public/styles/core_app/_globals_v7light.scss diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/core/public/core_app/styles/_globals_v8dark.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/core/public/styles/core_app/_globals_v8dark.scss similarity index 100% rename from packages/kbn-optimizer/src/__fixtures__/mock_repo/src/core/public/core_app/styles/_globals_v8dark.scss rename to packages/kbn-optimizer/src/__fixtures__/mock_repo/src/core/public/styles/core_app/_globals_v8dark.scss diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/core/public/core_app/styles/_globals_v8light.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/core/public/styles/core_app/_globals_v8light.scss similarity index 100% rename from packages/kbn-optimizer/src/__fixtures__/mock_repo/src/core/public/core_app/styles/_globals_v8light.scss rename to packages/kbn-optimizer/src/__fixtures__/mock_repo/src/core/public/styles/core_app/_globals_v8light.scss diff --git a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts index e7f9e7d3c3b81e..fdd36c76f6e4d9 100644 --- a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts @@ -164,8 +164,8 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/legacy/_other_styles.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/legacy/styles.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/lib.ts, - /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/core/public/core_app/styles/_globals_v8dark.scss, - /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/core/public/core_app/styles/_globals_v8light.scss, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/core/public/styles/core_app/_globals_v8dark.scss, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/core/public/styles/core_app/_globals_v8light.scss, /packages/kbn-optimizer/src/worker/entry_point_creator.ts, ] `); diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 1b87498496b243..04074fb2b10b40 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -188,7 +188,7 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: loaderContext, Path.resolve( worker.repoRoot, - `src/core/public/core_app/styles/_globals_${theme}.scss` + `src/core/public/styles/core_app/_globals_${theme}.scss` ) )};\n${content}`; }, @@ -246,7 +246,10 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: extensions: ['.js', '.ts', '.tsx', '.json'], mainFields: ['browser', 'main'], alias: { - core_app_image_assets: Path.resolve(worker.repoRoot, 'src/core/public/core_app/images'), + core_app_image_assets: Path.resolve( + worker.repoRoot, + 'src/core/public/styles/core_app/images' + ), vega: Path.resolve(worker.repoRoot, 'node_modules/vega/build-es5/vega.js'), }, symlinks: false, diff --git a/packages/kbn-storybook/src/webpack.config.ts b/packages/kbn-storybook/src/webpack.config.ts index 880afe83528a84..3d00fb88a089a6 100644 --- a/packages/kbn-storybook/src/webpack.config.ts +++ b/packages/kbn-storybook/src/webpack.config.ts @@ -102,7 +102,7 @@ export default ({ config: storybookConfig }: { config: Configuration }) => { additionalData(content: string, loaderContext: any) { return `@import ${stringifyRequest( loaderContext, - resolve(REPO_ROOT, 'src/core/public/core_app/styles/_globals_v8light.scss') + resolve(REPO_ROOT, 'src/core/public/styles/core_app/_globals_v8light.scss') )};\n${content}`; }, implementation: require('node-sass'), @@ -120,7 +120,7 @@ export default ({ config: storybookConfig }: { config: Configuration }) => { extensions: ['.js', '.ts', '.tsx', '.json', '.mdx'], mainFields: ['browser', 'main'], alias: { - core_app_image_assets: resolve(REPO_ROOT, 'src/core/public/core_app/images'), + core_app_image_assets: resolve(REPO_ROOT, 'src/core/public/styles/core_app/images'), core_styles: resolve(REPO_ROOT, 'src/core/public/index.scss'), }, symlinks: false, diff --git a/packages/kbn-test/src/es/test_es_cluster.ts b/packages/kbn-test/src/es/test_es_cluster.ts index 8c650ec9b60518..70fa5f2e8d3759 100644 --- a/packages/kbn-test/src/es/test_es_cluster.ts +++ b/packages/kbn-test/src/es/test_es_cluster.ts @@ -95,6 +95,7 @@ export interface CreateTestEsClusterOptions { */ license?: 'basic' | 'gold' | 'trial'; // | 'oss' log: ToolingLog; + writeLogsToPath?: string; /** * Node-specific configuration if you wish to run a multi-node * cluster. One node will be added for each item in the array. @@ -168,6 +169,7 @@ export function createTestEsCluster< password = 'changeme', license = 'basic', log, + writeLogsToPath, basePath = Path.resolve(REPO_ROOT, '.es'), esFrom = esTestConfig.getBuildFrom(), dataArchive, @@ -272,6 +274,7 @@ export function createTestEsCluster< skipNativeRealmSetup: this.nodes.length > 1 && i < this.nodes.length - 1, skipReadyCheck: this.nodes.length > 1 && i < this.nodes.length - 1, onEarlyExit, + writeLogsToPath, }); }); } diff --git a/packages/kbn-test/src/failed_tests_reporter/__fixtures__/cypress_report.xml b/packages/kbn-test/src/failed_tests_reporter/__fixtures__/cypress_report.xml index ed0e154552caac..c0c66e81db26d2 100644 --- a/packages/kbn-test/src/failed_tests_reporter/__fixtures__/cypress_report.xml +++ b/packages/kbn-test/src/failed_tests_reporter/__fixtures__/cypress_report.xml @@ -1,6 +1,6 @@ - + diff --git a/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts b/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts index 220a336915bf14..3b71823ee6bdeb 100644 --- a/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts +++ b/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts @@ -282,9 +282,9 @@ it('rewrites cypress reports with minimal changes', async () => { -‹?xml version="1.0" encoding="UTF-8"?› +‹?xml version="1.0" encoding="utf-8"?› ‹testsuites name="Mocha Tests" time="16.198" tests="2" failures="1"› - - ‹testsuite name="Root Suite" timestamp="2020-07-22T15:06:26" tests="0" file="cypress/integration/timeline_flyout_button.spec.ts" failures="0" time="0"› + - ‹testsuite name="Root Suite" timestamp="2020-07-22T15:06:26" tests="0" file="cypress/e2e/timeline_flyout_button.spec.ts" failures="0" time="0"› - ‹/testsuite› - + ‹testsuite name="Root Suite" timestamp="2020-07-22T15:06:26" tests="0" file="cypress/integration/timeline_flyout_button.spec.ts" failures="0" time="0"/› + + ‹testsuite name="Root Suite" timestamp="2020-07-22T15:06:26" tests="0" file="cypress/e2e/timeline_flyout_button.spec.ts" failures="0" time="0"/› ‹testsuite name="timeline flyout button" timestamp="2020-07-22T15:06:26" tests="2" failures="1" time="16.198"› - ‹testcase name="timeline flyout button toggles open the timeline" time="8.099" classname="toggles open the timeline"› - ‹/testcase› diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap index cff0b46afcad17..ff8961e263f170 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap @@ -23,6 +23,7 @@ Options: --include-tag Tags that suites must include to be run, can be included multiple times. --exclude-tag Tags that suites must NOT include to be run, can be included multiple times. --assert-none-excluded Exit with 1/0 based on if any test is excluded with the current set of tags. + --logToFile Write the log output from Kibana/Elasticsearch to files instead of to stdout --verbose Log everything. --debug Run in debug mode. --quiet Only log errors. @@ -40,6 +41,7 @@ Object { "esFrom": "snapshot", "esVersion": "999.999.999", "extraKbnOpts": undefined, + "logsDir": undefined, "suiteFiles": Object { "exclude": Array [], "include": Array [], @@ -62,6 +64,7 @@ Object { "esFrom": "snapshot", "esVersion": "999.999.999", "extraKbnOpts": undefined, + "logsDir": undefined, "suiteFiles": Object { "exclude": Array [], "include": Array [], @@ -85,6 +88,7 @@ Object { "esFrom": "snapshot", "esVersion": "999.999.999", "extraKbnOpts": undefined, + "logsDir": undefined, "suiteFiles": Object { "exclude": Array [], "include": Array [], @@ -107,6 +111,7 @@ Object { "esFrom": "snapshot", "esVersion": "999.999.999", "extraKbnOpts": undefined, + "logsDir": undefined, "suiteFiles": Object { "exclude": Array [], "include": Array [], @@ -133,6 +138,7 @@ Object { "extraKbnOpts": Object { "server.foo": "bar", }, + "logsDir": undefined, "suiteFiles": Object { "exclude": Array [], "include": Array [], @@ -154,6 +160,7 @@ Object { "esFrom": "snapshot", "esVersion": "999.999.999", "extraKbnOpts": undefined, + "logsDir": undefined, "quiet": true, "suiteFiles": Object { "exclude": Array [], @@ -176,6 +183,7 @@ Object { "esFrom": "snapshot", "esVersion": "999.999.999", "extraKbnOpts": undefined, + "logsDir": undefined, "silent": true, "suiteFiles": Object { "exclude": Array [], @@ -198,6 +206,7 @@ Object { "esFrom": "source", "esVersion": "999.999.999", "extraKbnOpts": undefined, + "logsDir": undefined, "suiteFiles": Object { "exclude": Array [], "include": Array [], @@ -219,6 +228,7 @@ Object { "esFrom": "source", "esVersion": "999.999.999", "extraKbnOpts": undefined, + "logsDir": undefined, "suiteFiles": Object { "exclude": Array [], "include": Array [], @@ -241,6 +251,7 @@ Object { "esVersion": "999.999.999", "extraKbnOpts": undefined, "installDir": "foo", + "logsDir": undefined, "suiteFiles": Object { "exclude": Array [], "include": Array [], @@ -263,6 +274,7 @@ Object { "esVersion": "999.999.999", "extraKbnOpts": undefined, "grep": "management", + "logsDir": undefined, "suiteFiles": Object { "exclude": Array [], "include": Array [], @@ -284,6 +296,7 @@ Object { "esFrom": "snapshot", "esVersion": "999.999.999", "extraKbnOpts": undefined, + "logsDir": undefined, "suiteFiles": Object { "exclude": Array [], "include": Array [], @@ -306,6 +319,7 @@ Object { "esFrom": "snapshot", "esVersion": "999.999.999", "extraKbnOpts": undefined, + "logsDir": undefined, "suiteFiles": Object { "exclude": Array [], "include": Array [], diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap deleted file mode 100644 index 6b81c2e499cf4c..00000000000000 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap +++ /dev/null @@ -1,74 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`run tests CLI options accepts help option even if invalid options passed 1`] = ` -"Run Functional Tests - -Usage: - node scripts/functional_tests --help - node scripts/functional_tests [--config [--config ...]] - node scripts/functional_tests [options] [-- --] - -Options: - --help Display this menu and exit. - --config Pass in a config. Can pass in multiple configs. - --esFrom Build Elasticsearch from source or run from snapshot. Default: $TEST_ES_FROM or snapshot - --kibana-install-dir

Run Kibana from existing install directory instead of from source. - --bail Stop the test run at the first failure. - --grep Pattern to select which tests to run. - --updateBaselines Replace baseline screenshots with whatever is generated from the test. - --updateSnapshots Replace inline and file snapshots with whatever is generated from the test. - --u Replace both baseline screenshots and snapshots - --include Files that must included to be run, can be included multiple times. - --exclude Files that must NOT be included to be run, can be included multiple times. - --include-tag Tags that suites must include to be run, can be included multiple times. - --exclude-tag Tags that suites must NOT include to be run, can be included multiple times. - --assert-none-excluded Exit with 1/0 based on if any test is excluded with the current set of tags. - --verbose Log everything. - --debug Run in debug mode. - --quiet Only log errors. - --silent Log nothing. - --dry-run Report tests without executing them. -" -`; - -exports[`run tests CLI options rejects boolean config value 1`] = ` -" -functional_tests: invalid argument [true] to option [config] - ...stack trace... -" -`; - -exports[`run tests CLI options rejects boolean value for kibana-install-dir 1`] = ` -" -functional_tests: invalid argument [true] to option [kibana-install-dir] - ...stack trace... -" -`; - -exports[`run tests CLI options rejects empty config value if no default passed 1`] = ` -" -functional_tests: config is required - ...stack trace... -" -`; - -exports[`run tests CLI options rejects invalid options even if valid options exist 1`] = ` -" -functional_tests: invalid option [aintnothang] - ...stack trace... -" -`; - -exports[`run tests CLI options rejects non-boolean value for bail 1`] = ` -" -functional_tests: invalid argument [peanut] to option [bail] - ...stack trace... -" -`; - -exports[`run tests CLI options rejects non-enum value for esFrom 1`] = ` -" -functional_tests: invalid argument [butter] to option [esFrom] - ...stack trace... -" -`; diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/args.js b/packages/kbn-test/src/functional_tests/cli/run_tests/args.js index d94adcfe615a5d..8b1bf471f4e98f 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/args.js +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/args.js @@ -6,9 +6,11 @@ * Side Public License, v 1. */ -import { resolve } from 'path'; +import Path from 'path'; +import { v4 as uuid } from 'uuid'; import dedent from 'dedent'; +import { REPO_ROOT } from '@kbn/utils'; import { ToolingLog, pickLevelFromFlags } from '@kbn/tooling-log'; import { EsVersion } from '../../../functional_test_runner'; @@ -61,6 +63,9 @@ const options = { 'assert-none-excluded': { desc: 'Exit with 1/0 based on if any test is excluded with the current set of tags.', }, + logToFile: { + desc: 'Write the log output from Kibana/Elasticsearch to files instead of to stdout', + }, verbose: { desc: 'Log everything.' }, debug: { desc: 'Run in debug mode.' }, quiet: { desc: 'Only log errors.' }, @@ -142,19 +147,24 @@ export function processOptions(userOptions, defaultConfigPaths) { delete userOptions['dry-run']; } + const log = new ToolingLog({ + level: pickLevelFromFlags(userOptions), + writeTo: process.stdout, + }); function createLogger() { - return new ToolingLog({ - level: pickLevelFromFlags(userOptions), - writeTo: process.stdout, - }); + return log; } + const logToFile = !!userOptions.logToFile; + const logsDir = logToFile ? Path.resolve(REPO_ROOT, 'data/ftr_servers_logs', uuid()) : undefined; + return { ...userOptions, - configs: configs.map((c) => resolve(c)), + configs: configs.map((c) => Path.resolve(c)), createLogger, extraKbnOpts: userOptions._, esVersion: EsVersion.getDefault(), + logsDir, }; } diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/cli.js b/packages/kbn-test/src/functional_tests/cli/run_tests/cli.js index e920e43f375b4b..3958c1503cd30b 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/cli.js +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/cli.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { runTests } from '../../tasks'; +import { runTests, initLogsDir } from '../../tasks'; import { runCli } from '../../lib'; import { processOptions, displayHelp } from './args'; @@ -21,6 +21,7 @@ import { processOptions, displayHelp } from './args'; export async function runTestsCli(defaultConfigPaths) { await runCli(displayHelp, async (userOptions) => { const options = processOptions(userOptions, defaultConfigPaths); + initLogsDir(options); await runTests(options); }); } diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/cli.test.js b/packages/kbn-test/src/functional_tests/cli/run_tests/cli.test.js deleted file mode 100644 index 1b679f285d1332..00000000000000 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/cli.test.js +++ /dev/null @@ -1,232 +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 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 or the Server - * Side Public License, v 1. - */ - -import { Writable } from 'stream'; - -import { runTestsCli } from './cli'; -import { checkMockConsoleLogSnapshot } from '../../test_helpers'; - -// Note: Stub the runTests function to keep testing only around the cli -// method and arguments. -jest.mock('../../tasks', () => ({ - runTests: jest.fn(), -})); - -describe('run tests CLI', () => { - describe('options', () => { - const originalObjects = { process, console }; - const exitMock = jest.fn(); - const logMock = jest.fn(); // mock logging so we don't send output to the test results - const argvMock = ['foo', 'foo']; - - const processMock = { - exit: exitMock, - argv: argvMock, - stdout: new Writable(), - cwd: jest.fn(), - env: { - ...originalObjects.process.env, - TEST_ES_FROM: 'snapshot', - }, - }; - - beforeAll(() => { - global.process = processMock; - global.console = { log: logMock }; - }); - - afterAll(() => { - global.process = originalObjects.process; - global.console = originalObjects.console; - }); - - beforeEach(() => { - global.process.argv = [...argvMock]; - global.process.env = { - ...originalObjects.process.env, - TEST_ES_FROM: 'snapshot', - }; - jest.resetAllMocks(); - }); - - it('rejects boolean config value', async () => { - global.process.argv.push('--config'); - - await runTestsCli(); - - expect(exitMock).toHaveBeenCalledWith(1); - checkMockConsoleLogSnapshot(logMock); - }); - - it('rejects empty config value if no default passed', async () => { - global.process.argv.push('--config', ''); - - await runTestsCli(); - - expect(exitMock).toHaveBeenCalledWith(1); - checkMockConsoleLogSnapshot(logMock); - }); - - it('accepts empty config value if default passed', async () => { - global.process.argv.push('--config', ''); - - await runTestsCli(['foo']); - - expect(exitMock).not.toHaveBeenCalled(); - }); - - it('rejects non-boolean value for bail', async () => { - global.process.argv.push('--bail', 'peanut'); - - await runTestsCli(['foo']); - - expect(exitMock).toHaveBeenCalledWith(1); - checkMockConsoleLogSnapshot(logMock); - }); - - it('accepts string value for kibana-install-dir', async () => { - global.process.argv.push('--kibana-install-dir', 'foo'); - - await runTestsCli(['foo']); - - expect(exitMock).not.toHaveBeenCalled(); - }); - - it('rejects boolean value for kibana-install-dir', async () => { - global.process.argv.push('--kibana-install-dir'); - - await runTestsCli(['foo']); - - expect(exitMock).toHaveBeenCalledWith(1); - checkMockConsoleLogSnapshot(logMock); - }); - - it('accepts boolean value for updateBaselines', async () => { - global.process.argv.push('--updateBaselines'); - - await runTestsCli(['foo']); - - expect(exitMock).not.toHaveBeenCalledWith(); - }); - - it('accepts boolean value for updateSnapshots', async () => { - global.process.argv.push('--updateSnapshots'); - - await runTestsCli(['foo']); - - expect(exitMock).not.toHaveBeenCalledWith(); - }); - - it('accepts boolean value for -u', async () => { - global.process.argv.push('-u'); - - await runTestsCli(['foo']); - - expect(exitMock).not.toHaveBeenCalledWith(); - }); - - it('accepts source value for esFrom', async () => { - global.process.argv.push('--esFrom', 'source'); - - await runTestsCli(['foo']); - - expect(exitMock).not.toHaveBeenCalled(); - }); - - it('rejects non-enum value for esFrom', async () => { - global.process.argv.push('--esFrom', 'butter'); - - await runTestsCli(['foo']); - - expect(exitMock).toHaveBeenCalledWith(1); - checkMockConsoleLogSnapshot(logMock); - }); - - it('accepts value for grep', async () => { - global.process.argv.push('--grep', 'management'); - - await runTestsCli(['foo']); - - expect(exitMock).not.toHaveBeenCalled(); - }); - - it('accepts debug option', async () => { - global.process.argv.push('--debug'); - - await runTestsCli(['foo']); - - expect(exitMock).not.toHaveBeenCalled(); - }); - - it('accepts silent option', async () => { - global.process.argv.push('--silent'); - - await runTestsCli(['foo']); - - expect(exitMock).not.toHaveBeenCalled(); - }); - - it('accepts quiet option', async () => { - global.process.argv.push('--quiet'); - - await runTestsCli(['foo']); - - expect(exitMock).not.toHaveBeenCalled(); - }); - - it('accepts verbose option', async () => { - global.process.argv.push('--verbose'); - - await runTestsCli(['foo']); - - expect(exitMock).not.toHaveBeenCalled(); - }); - - it('accepts network throttle option', async () => { - global.process.argv.push('--throttle'); - - await runTestsCli(['foo']); - - expect(exitMock).toHaveBeenCalledWith(1); - }); - - it('accepts headless option', async () => { - global.process.argv.push('--headless'); - - await runTestsCli(['foo']); - - expect(exitMock).toHaveBeenCalledWith(1); - }); - - it('accepts extra server options', async () => { - global.process.argv.push('--', '--server.foo=bar'); - - await runTestsCli(['foo']); - - expect(exitMock).not.toHaveBeenCalled(); - }); - - it('accepts help option even if invalid options passed', async () => { - global.process.argv.push('--debug', '--aintnothang', '--help'); - - await runTestsCli(['foo']); - - expect(exitMock).not.toHaveBeenCalledWith(1); - checkMockConsoleLogSnapshot(logMock); - }); - - it('rejects invalid options even if valid options exist', async () => { - global.process.argv.push('--debug', '--aintnothang', '--bail'); - - await runTestsCli(['foo']); - - expect(exitMock).toHaveBeenCalledWith(1); - checkMockConsoleLogSnapshot(logMock); - }); - }); -}); diff --git a/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/args.test.js.snap b/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/args.test.js.snap index cd3174d13c3e69..1f572578119f77 100644 --- a/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/args.test.js.snap +++ b/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/args.test.js.snap @@ -13,6 +13,7 @@ Options: --config Pass in a config --esFrom Build Elasticsearch from source, snapshot or path to existing install dir. Default: $TEST_ES_FROM or snapshot --kibana-install-dir Run Kibana from existing install directory instead of from source. + --logToFile Write the log output from Kibana/Elasticsearch to files instead of to stdout --verbose Log everything. --debug Run in debug mode. --quiet Only log errors. @@ -26,6 +27,7 @@ Object { "debug": true, "esFrom": "snapshot", "extraKbnOpts": undefined, + "logsDir": undefined, "useDefaultConfig": true, } `; @@ -36,6 +38,7 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "logsDir": undefined, "useDefaultConfig": true, } `; @@ -51,6 +54,7 @@ Object { "extraKbnOpts": Object { "server.foo": "bar", }, + "logsDir": undefined, "useDefaultConfig": true, } `; @@ -61,6 +65,7 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "logsDir": undefined, "quiet": true, "useDefaultConfig": true, } @@ -72,6 +77,7 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "logsDir": undefined, "silent": true, "useDefaultConfig": true, } @@ -83,6 +89,7 @@ Object { "createLogger": [Function], "esFrom": "source", "extraKbnOpts": undefined, + "logsDir": undefined, "useDefaultConfig": true, } `; @@ -93,6 +100,7 @@ Object { "createLogger": [Function], "esFrom": "source", "extraKbnOpts": undefined, + "logsDir": undefined, "useDefaultConfig": true, } `; @@ -104,6 +112,7 @@ Object { "esFrom": "snapshot", "extraKbnOpts": undefined, "installDir": "foo", + "logsDir": undefined, "useDefaultConfig": true, } `; @@ -114,6 +123,7 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "logsDir": undefined, "useDefaultConfig": true, "verbose": true, } @@ -125,6 +135,7 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "logsDir": undefined, "useDefaultConfig": true, } `; diff --git a/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/cli.test.js.snap b/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/cli.test.js.snap deleted file mode 100644 index ba085b08682169..00000000000000 --- a/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/cli.test.js.snap +++ /dev/null @@ -1,50 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`start servers CLI options accepts boolean value for updateBaselines 1`] = ` -" -functional_tests_server: invalid option [updateBaselines] - ...stack trace... -" -`; - -exports[`start servers CLI options accepts boolean value for updateSnapshots 1`] = ` -" -functional_tests_server: invalid option [updateSnapshots] - ...stack trace... -" -`; - -exports[`start servers CLI options rejects bail 1`] = ` -" -functional_tests_server: invalid option [bail] - ...stack trace... -" -`; - -exports[`start servers CLI options rejects boolean config value 1`] = ` -" -functional_tests_server: invalid argument [true] to option [config] - ...stack trace... -" -`; - -exports[`start servers CLI options rejects boolean value for kibana-install-dir 1`] = ` -" -functional_tests_server: invalid argument [true] to option [kibana-install-dir] - ...stack trace... -" -`; - -exports[`start servers CLI options rejects empty config value if no default passed 1`] = ` -" -functional_tests_server: config is required - ...stack trace... -" -`; - -exports[`start servers CLI options rejects invalid options even if valid options exist 1`] = ` -" -functional_tests_server: invalid option [grep] - ...stack trace... -" -`; diff --git a/packages/kbn-test/src/functional_tests/cli/start_servers/args.js b/packages/kbn-test/src/functional_tests/cli/start_servers/args.js index 527e3ce64613dc..e025bdc3393318 100644 --- a/packages/kbn-test/src/functional_tests/cli/start_servers/args.js +++ b/packages/kbn-test/src/functional_tests/cli/start_servers/args.js @@ -6,9 +6,11 @@ * Side Public License, v 1. */ -import { resolve } from 'path'; +import Path from 'path'; +import { v4 as uuid } from 'uuid'; import dedent from 'dedent'; +import { REPO_ROOT } from '@kbn/utils'; import { ToolingLog, pickLevelFromFlags } from '@kbn/tooling-log'; const options = { @@ -26,6 +28,9 @@ const options = { arg: '', desc: 'Run Kibana from existing install directory instead of from source.', }, + logToFile: { + desc: 'Write the log output from Kibana/Elasticsearch to files instead of to stdout', + }, verbose: { desc: 'Log everything.' }, debug: { desc: 'Run in debug mode.' }, quiet: { desc: 'Only log errors.' }, @@ -80,16 +85,22 @@ export function processOptions(userOptions, defaultConfigPath) { delete userOptions['kibana-install-dir']; } + const log = new ToolingLog({ + level: pickLevelFromFlags(userOptions), + writeTo: process.stdout, + }); + function createLogger() { - return new ToolingLog({ - level: pickLevelFromFlags(userOptions), - writeTo: process.stdout, - }); + return log; } + const logToFile = !!userOptions.logToFile; + const logsDir = logToFile ? Path.resolve(REPO_ROOT, 'data/ftr_servers_logs', uuid()) : undefined; + return { ...userOptions, - config: resolve(config), + logsDir, + config: Path.resolve(config), useDefaultConfig, createLogger, extraKbnOpts: userOptions._, diff --git a/packages/kbn-test/src/functional_tests/cli/start_servers/cli.js b/packages/kbn-test/src/functional_tests/cli/start_servers/cli.js index df7f8750b2ae31..d57d5c4761f6ea 100644 --- a/packages/kbn-test/src/functional_tests/cli/start_servers/cli.js +++ b/packages/kbn-test/src/functional_tests/cli/start_servers/cli.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { startServers } from '../../tasks'; +import { startServers, initLogsDir } from '../../tasks'; import { runCli } from '../../lib'; import { processOptions, displayHelp } from './args'; @@ -18,6 +18,7 @@ import { processOptions, displayHelp } from './args'; export async function startServersCli(defaultConfigPath) { await runCli(displayHelp, async (userOptions) => { const options = processOptions(userOptions, defaultConfigPath); + initLogsDir(options); await startServers({ ...options, }); diff --git a/packages/kbn-test/src/functional_tests/cli/start_servers/cli.test.js b/packages/kbn-test/src/functional_tests/cli/start_servers/cli.test.js deleted file mode 100644 index a88e4dbd011692..00000000000000 --- a/packages/kbn-test/src/functional_tests/cli/start_servers/cli.test.js +++ /dev/null @@ -1,192 +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 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 or the Server - * Side Public License, v 1. - */ - -import { Writable } from 'stream'; - -import { startServersCli } from './cli'; -import { checkMockConsoleLogSnapshot } from '../../test_helpers'; - -// Note: Stub the startServers function to keep testing only around the cli -// method and arguments. -jest.mock('../../tasks', () => ({ - startServers: jest.fn(), -})); - -describe('start servers CLI', () => { - describe('options', () => { - const originalObjects = { process, console }; - const exitMock = jest.fn(); - const logMock = jest.fn(); // mock logging so we don't send output to the test results - const argvMock = ['foo', 'foo']; - - const processMock = { - exit: exitMock, - argv: argvMock, - stdout: new Writable(), - cwd: jest.fn(), - env: { - ...originalObjects.process.env, - TEST_ES_FROM: 'snapshot', - }, - }; - - beforeAll(() => { - global.process = processMock; - global.console = { log: logMock }; - }); - - afterAll(() => { - global.process = originalObjects.process; - global.console = originalObjects.console; - }); - - beforeEach(() => { - global.process.argv = [...argvMock]; - global.process.env = { - ...originalObjects.process.env, - TEST_ES_FROM: 'snapshot', - }; - jest.resetAllMocks(); - }); - - it('rejects boolean config value', async () => { - global.process.argv.push('--config'); - - await startServersCli(); - - expect(exitMock).toHaveBeenCalledWith(1); - checkMockConsoleLogSnapshot(logMock); - }); - - it('rejects empty config value if no default passed', async () => { - global.process.argv.push('--config', ''); - - await startServersCli(); - - expect(exitMock).toHaveBeenCalledWith(1); - checkMockConsoleLogSnapshot(logMock); - }); - - it('accepts empty config value if default passed', async () => { - global.process.argv.push('--config', ''); - - await startServersCli('foo'); - - expect(exitMock).not.toHaveBeenCalled(); - }); - - it('rejects bail', async () => { - global.process.argv.push('--bail', true); - - await startServersCli('foo'); - - expect(exitMock).toHaveBeenCalledWith(1); - checkMockConsoleLogSnapshot(logMock); - }); - - it('accepts string value for kibana-install-dir', async () => { - global.process.argv.push('--kibana-install-dir', 'foo'); - - await startServersCli('foo'); - - expect(exitMock).not.toHaveBeenCalled(); - }); - - it('rejects boolean value for kibana-install-dir', async () => { - global.process.argv.push('--kibana-install-dir'); - - await startServersCli('foo'); - - expect(exitMock).toHaveBeenCalledWith(1); - checkMockConsoleLogSnapshot(logMock); - }); - - it('accepts boolean value for updateBaselines', async () => { - global.process.argv.push('--updateBaselines'); - - await startServersCli('foo'); - - expect(exitMock).toHaveBeenCalledWith(1); - checkMockConsoleLogSnapshot(logMock); - }); - - it('accepts boolean value for updateSnapshots', async () => { - global.process.argv.push('--updateSnapshots'); - - await startServersCli('foo'); - - expect(exitMock).toHaveBeenCalledWith(1); - checkMockConsoleLogSnapshot(logMock); - }); - - it('accepts source value for esFrom', async () => { - global.process.argv.push('--esFrom', 'source'); - - await startServersCli('foo'); - - expect(exitMock).not.toHaveBeenCalled(); - }); - - it('accepts debug option', async () => { - global.process.argv.push('--debug'); - - await startServersCli('foo'); - - expect(exitMock).not.toHaveBeenCalled(); - }); - - it('accepts silent option', async () => { - global.process.argv.push('--silent'); - - await startServersCli('foo'); - - expect(exitMock).not.toHaveBeenCalled(); - }); - - it('accepts quiet option', async () => { - global.process.argv.push('--quiet'); - - await startServersCli('foo'); - - expect(exitMock).not.toHaveBeenCalled(); - }); - - it('accepts verbose option', async () => { - global.process.argv.push('--verbose'); - - await startServersCli('foo'); - - expect(exitMock).not.toHaveBeenCalled(); - }); - - it('accepts extra server options', async () => { - global.process.argv.push('--', '--server.foo=bar'); - - await startServersCli('foo'); - - expect(exitMock).not.toHaveBeenCalled(); - }); - - it('accepts help option even if invalid options passed', async () => { - global.process.argv.push('--debug', '--grep', '--help'); - - await startServersCli('foo'); - - expect(exitMock).not.toHaveBeenCalledWith(1); - }); - - it('rejects invalid options even if valid options exist', async () => { - global.process.argv.push('--debug', '--grep', '--bail'); - - await startServersCli('foo'); - - expect(exitMock).toHaveBeenCalledWith(1); - checkMockConsoleLogSnapshot(logMock); - }); - }); -}); diff --git a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts index 5dcee56e765e0d..b367af4daf4921 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts +++ b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts @@ -18,6 +18,7 @@ interface RunElasticsearchOptions { esFrom?: string; config: Config; onEarlyExit?: (msg: string) => void; + logsDir?: string; } interface CcsConfig { @@ -62,26 +63,41 @@ function getEsConfig({ export async function runElasticsearch( options: RunElasticsearchOptions ): Promise<() => Promise> { - const { log } = options; + const { log, logsDir } = options; const config = getEsConfig(options); if (!config.ccsConfig) { - const node = await startEsNode(log, 'ftr', config); + const node = await startEsNode({ + log, + name: 'ftr', + logsDir, + config, + }); return async () => { await node.cleanup(); }; } const remotePort = await getPort(); - const remoteNode = await startEsNode(log, 'ftr-remote', { - ...config, - port: parseInt(new URL(config.ccsConfig.remoteClusterUrl).port, 10), - transportPort: remotePort, + const remoteNode = await startEsNode({ + log, + name: 'ftr-remote', + logsDir, + config: { + ...config, + port: parseInt(new URL(config.ccsConfig.remoteClusterUrl).port, 10), + transportPort: remotePort, + }, }); - const localNode = await startEsNode(log, 'ftr-local', { - ...config, - esArgs: [...config.esArgs, `cluster.remote.ftr-remote.seeds=localhost:${remotePort}`], + const localNode = await startEsNode({ + log, + name: 'ftr-local', + logsDir, + config: { + ...config, + esArgs: [...config.esArgs, `cluster.remote.ftr-remote.seeds=localhost:${remotePort}`], + }, }); return async () => { @@ -90,12 +106,19 @@ export async function runElasticsearch( }; } -async function startEsNode( - log: ToolingLog, - name: string, - config: EsConfig & { transportPort?: number }, - onEarlyExit?: (msg: string) => void -) { +async function startEsNode({ + log, + name, + config, + onEarlyExit, + logsDir, +}: { + log: ToolingLog; + name: string; + config: EsConfig & { transportPort?: number }; + onEarlyExit?: (msg: string) => void; + logsDir?: string; +}) { const cluster = createTestEsCluster({ clusterName: `cluster-${name}`, esArgs: config.esArgs, @@ -106,6 +129,7 @@ async function startEsNode( port: config.port, ssl: config.ssl, log, + writeLogsToPath: logsDir ? resolve(logsDir, `es-cluster-${name}.log`) : undefined, basePath: resolve(REPO_ROOT, '.es'), nodes: [ { diff --git a/packages/kbn-test/src/functional_tests/lib/run_kibana_server.ts b/packages/kbn-test/src/functional_tests/lib/run_kibana_server.ts index 58b77151a9fdeb..2ae15ca5f83f86 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_kibana_server.ts +++ b/packages/kbn-test/src/functional_tests/lib/run_kibana_server.ts @@ -42,7 +42,11 @@ export async function runKibanaServer({ }: { procs: ProcRunner; config: Config; - options: { installDir?: string; extraKbnOpts?: string[] }; + options: { + installDir?: string; + extraKbnOpts?: string[]; + logsDir?: string; + }; onEarlyExit?: (msg: string) => void; }) { const runOptions = config.get('kbnTestServer.runOptions'); @@ -84,10 +88,14 @@ export async function runKibanaServer({ ...(options.extraKbnOpts ?? []), ]); + const mainName = useTaskRunner ? 'kbn-ui' : 'kibana'; const promises = [ // main process - procs.run(useTaskRunner ? 'kbn-ui' : 'kibana', { + procs.run(mainName, { ...procRunnerOpts, + writeLogsToPath: options.logsDir + ? Path.resolve(options.logsDir, `${mainName}.log`) + : undefined, args: [ ...prefixArgs, ...parseRawFlags([ @@ -110,6 +118,9 @@ export async function runKibanaServer({ promises.push( procs.run('kbn-tasks', { ...procRunnerOpts, + writeLogsToPath: options.logsDir + ? Path.resolve(options.logsDir, 'kbn-tasks.log') + : undefined, args: [ ...prefixArgs, ...parseRawFlags([ diff --git a/packages/kbn-test/src/functional_tests/tasks.ts b/packages/kbn-test/src/functional_tests/tasks.ts index 9b5fb5424f3feb..26504b07544b0f 100644 --- a/packages/kbn-test/src/functional_tests/tasks.ts +++ b/packages/kbn-test/src/functional_tests/tasks.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import Fs from 'fs'; import Path from 'path'; import { setTimeout } from 'timers/promises'; @@ -51,6 +52,16 @@ const makeSuccessMessage = (options: StartServerOptions) => { ); }; +export async function initLogsDir(options: { logsDir?: string; createLogger(): ToolingLog }) { + if (options.logsDir) { + options + .createLogger() + .info(`Kibana/ES logs will be written to ${Path.relative(process.cwd(), options.logsDir)}/`); + + Fs.mkdirSync(options.logsDir, { recursive: true }); + } +} + /** * Run servers and tests for each config */ diff --git a/packages/kbn-user-profile-components/src/user_profiles_popover.test.tsx b/packages/kbn-user-profile-components/src/user_profiles_popover.test.tsx index 6f838631591cee..a68f09cba6a548 100644 --- a/packages/kbn-user-profile-components/src/user_profiles_popover.test.tsx +++ b/packages/kbn-user-profile-components/src/user_profiles_popover.test.tsx @@ -8,10 +8,11 @@ import { shallow } from 'enzyme'; import React from 'react'; +import { UserProfile } from './user_profile'; import { UserProfilesPopover } from './user_profiles_popover'; -const userProfiles = [ +const userProfiles: UserProfile[] = [ { uid: 'u_BOulL4QMPSyV9jg5lQI2JmCkUnokHTazBnet3xVHNv0_0', enabled: true, diff --git a/packages/kbn-user-profile-components/src/user_profiles_selectable.test.tsx b/packages/kbn-user-profile-components/src/user_profiles_selectable.test.tsx index ad3b3c94ac6dad..05eb4966496a05 100644 --- a/packages/kbn-user-profile-components/src/user_profiles_selectable.test.tsx +++ b/packages/kbn-user-profile-components/src/user_profiles_selectable.test.tsx @@ -8,10 +8,11 @@ import { mount } from 'enzyme'; import React from 'react'; +import { UserProfile } from './user_profile'; import { UserProfilesSelectable } from './user_profiles_selectable'; -const userProfiles = [ +const userProfiles: UserProfile[] = [ { uid: 'u_BOulL4QMPSyV9jg5lQI2JmCkUnokHTazBnet3xVHNv0_0', enabled: true, diff --git a/packages/shared-ux/router/impl/BUILD.bazel b/packages/shared-ux/router/impl/BUILD.bazel new file mode 100644 index 00000000000000..bc9b0aaac6d381 --- /dev/null +++ b/packages/shared-ux/router/impl/BUILD.bazel @@ -0,0 +1,140 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "shared-ux-router" +PKG_REQUIRE_NAME = "@kbn/shared-ux-router" + +SOURCE_FILES = glob( + [ + "**/*.ts", + "**/*.tsx", + "**/*.mdx" + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "@npm//react", + "@npm//react-router-dom", + "@npm//react-use", + "@npm//rxjs", + "//packages/kbn-shared-ux-utility", + "//packages/kbn-test-jest-helpers", +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "@npm//@types/react", + "@npm//@types/react-router-dom", + "@npm//react-use", + "@npm//rxjs", + "//packages/kbn-shared-ux-utility:npm_module_types", + "//packages/shared-ux/router/types:npm_module_types", + "//packages/kbn-ambient-ui-types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, + additional_args = [ + "--copy-files", + "--quiet" + ], +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + declaration_map = True, + emit_declaration_only = True, + out_dir = "target_types", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/shared-ux/router/impl/README.mdx b/packages/shared-ux/router/impl/README.mdx new file mode 100644 index 00000000000000..b8b0235e9a1e46 --- /dev/null +++ b/packages/shared-ux/router/impl/README.mdx @@ -0,0 +1,53 @@ +--- +id: sharedUX/Router +slug: /shared-ux/router +title: Router +description: A router component +tags: ['shared-ux', 'component', 'router', 'route'] +date: 2022-08-12 +--- + +## Summary +This is a wrapper around the `react-router-dom` Route component that inserts MatchPropagator in every application route. It helps track all route changes and send them to the execution context, later used to enrich APM 'route-change' transactions. +The component does not require any props and accepts props from the RouteProps interface such as a `path`, or a component like `AppContainer`. + + +```jsx + +``` + +### Explanation of RouteProps + +```jsx +export interface RouteProps { + location?: H.Location; + component?: React.ComponentType> | React.ComponentType; + render?: (props: RouteComponentProps) => React.ReactNode; + children?: ((props: RouteChildrenProps) => React.ReactNode) | React.ReactNode; + path?: string | string[]; + exact?: boolean; + sensitive?: boolean; + strict?: boolean; +} +``` + +All props are optional + +| Prop Name | Prop Type | Description | +|---|---|---| +| `location` | `H.Location` | the location of one instance of history | +| `component` | `React.ComponentType>` or `React.ComponentType;` | a react component | +| `render` | `(props: RouteComponentProps) => React.ReactNode;` | render props to a react node| +| `children` | `((props: RouteChildrenProps) => React.ReactNode)` or `React.ReactNode;` | pass children to a react node | +| `path` | `string` or `string[];` | a url path or array of paths | +| `exact` | `boolean` | exact match for a route (see: https://stackoverflow.com/questions/52275146/usage-of-exact-and-strict-props) | +| `sensitive` | `boolean` | case senstive route | +| `strict` | `boolean` | strict entry of the requested path in the path name | + + + +This component removes the need for manual calls to `useExecutionContext` and they should be removed. + +## EUI Promotion Status + +This component is not currently considered for promotion to EUI. \ No newline at end of file diff --git a/packages/shared-ux/router/impl/__snapshots__/router.test.tsx.snap b/packages/shared-ux/router/impl/__snapshots__/router.test.tsx.snap new file mode 100644 index 00000000000000..418aa60b7c1f4f --- /dev/null +++ b/packages/shared-ux/router/impl/__snapshots__/router.test.tsx.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Route component prop renders 1`] = ` + +`; + +exports[`Route location renders as expected 1`] = ` + + + +`; + +exports[`Route render prop renders 1`] = ` + +`; + +exports[`Route renders 1`] = ` + + + +`; diff --git a/test/visual_regression/services/visual_testing/index.ts b/packages/shared-ux/router/impl/index.ts similarity index 91% rename from test/visual_regression/services/visual_testing/index.ts rename to packages/shared-ux/router/impl/index.ts index 156e3814d8a1d0..8659ff73ced365 100644 --- a/test/visual_regression/services/visual_testing/index.ts +++ b/packages/shared-ux/router/impl/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export * from './visual_testing'; +export { Route } from './router'; diff --git a/packages/shared-ux/router/impl/jest.config.js b/packages/shared-ux/router/impl/jest.config.js new file mode 100644 index 00000000000000..fe0025102d6550 --- /dev/null +++ b/packages/shared-ux/router/impl/jest.config.js @@ -0,0 +1,13 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/packages/shared-ux/router/impl'], +}; diff --git a/packages/shared-ux/router/impl/package.json b/packages/shared-ux/router/impl/package.json new file mode 100644 index 00000000000000..3faa6ac609ebc0 --- /dev/null +++ b/packages/shared-ux/router/impl/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/shared-ux-router", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/shared-ux/router/impl/router.test.tsx b/packages/shared-ux/router/impl/router.test.tsx new file mode 100644 index 00000000000000..8c068d5a162d0e --- /dev/null +++ b/packages/shared-ux/router/impl/router.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ + +import React, { Component, FC } from 'react'; +import { shallow } from 'enzyme'; +import { Route } from './router'; +import { createMemoryHistory } from 'history'; + +describe('Route', () => { + test('renders', () => { + const example = shallow(); + expect(example).toMatchSnapshot(); + }); + + test('location renders as expected', () => { + // create a history + const historyLocation = createMemoryHistory(); + // add the path to the history + historyLocation.push('/app/wow'); + // prevent the location key from remaking itself each jest test + historyLocation.location.key = 's5brde'; + // the Route component takes the history location + const example = shallow(); + expect(example).toMatchSnapshot(); + }); + + test('component prop renders', () => { + const sampleComponent: FC<{}> = () => { + return Test; + }; + const example = shallow(); + expect(example).toMatchSnapshot(); + }); + + test('render prop renders', () => { + const sampleReactNode = React.createElement('li', { id: 'li1' }, 'one'); + const example = shallow( sampleReactNode} />); + expect(example).toMatchSnapshot(); + }); +}); diff --git a/packages/shared-ux/router/impl/router.tsx b/packages/shared-ux/router/impl/router.tsx new file mode 100644 index 00000000000000..da1dc2def3fc85 --- /dev/null +++ b/packages/shared-ux/router/impl/router.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ + +import React, { useMemo } from 'react'; +import { + Route as ReactRouterRoute, + RouteComponentProps, + RouteProps, + useRouteMatch, +} from 'react-router-dom'; +import { useKibanaSharedUX } from './services'; +import { useSharedUXExecutionContext } from './use_execution_context'; + +/** + * This is a wrapper around the react-router-dom Route component that inserts + * MatchPropagator in every application route. It helps track all route changes + * and send them to the execution context, later used to enrich APM + * 'route-change' transactions. + */ +export const Route = ({ children, component: Component, render, ...rest }: RouteProps) => { + const component = useMemo(() => { + if (!Component) { + return undefined; + } + return (props: RouteComponentProps) => ( + <> + + + + ); + }, [Component]); + + if (component) { + return ; + } + if (render || typeof children === 'function') { + const renderFunction = typeof children === 'function' ? children : render; + return ( + ( + <> + + {/* @ts-ignore else condition exists if renderFunction is undefined*/} + {renderFunction(props)} + + )} + /> + ); + } + return ( + + + {children} + + ); +}; + +/** + * The match propogator that is part of the Route + */ +const MatchPropagator = () => { + const { executionContext } = useKibanaSharedUX().services; + const match = useRouteMatch(); + + useSharedUXExecutionContext(executionContext, { + type: 'application', + page: match.path, + id: Object.keys(match.params).length > 0 ? JSON.stringify(match.params) : undefined, + }); + + return null; +}; diff --git a/packages/shared-ux/router/impl/services.ts b/packages/shared-ux/router/impl/services.ts new file mode 100644 index 00000000000000..78150b576905b8 --- /dev/null +++ b/packages/shared-ux/router/impl/services.ts @@ -0,0 +1,73 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { Observable } from 'rxjs'; +import { createContext, useContext } from 'react'; +import { SharedUXExecutionContext } from './types'; + +/** + * @public Execution context start and setup types are the same + */ +export declare type SharedUXExecutionContextStart = SharedUXExecutionContextSetup; + +/** + * Reduced the interface from ExecutionContextSetup from '@kbn/core-execution-context-browser' to only include properties needed for the Route + */ +export interface SharedUXExecutionContextSetup { + /** + * The current context observable + **/ + context$: Observable; + /** + * Set the current top level context + **/ + set(c$: SharedUXExecutionContext): void; + /** + * Get the current top level context + **/ + get(): SharedUXExecutionContext; + /** + * clears the context + **/ + clear(): void; +} + +/** + * Taken from Core services exposed to the `Plugin` start lifecycle + * + * @public + * + * @internalRemarks We document the properties with + * \@link tags to improve + * navigation in the generated docs until there's a fix for + * https://github.com/Microsoft/web-build-tools/issues/1237 + */ +export interface SharedUXExecutionContextSetup { + /** {@link SharedUXExecutionContextSetup} */ + executionContext: SharedUXExecutionContextStart; +} + +export type KibanaServices = Partial; + +export interface SharedUXRouterContextValue { + readonly services: Services; +} + +const defaultContextValue = { + services: {}, +}; + +export const sharedUXContext = + createContext>(defaultContextValue); + +export const useKibanaSharedUX = (): SharedUXRouterContextValue< + KibanaServices & Extra +> => + useContext( + sharedUXContext as unknown as React.Context> + ); diff --git a/packages/shared-ux/router/impl/tsconfig.json b/packages/shared-ux/router/impl/tsconfig.json new file mode 100644 index 00000000000000..764f1f42f52f9d --- /dev/null +++ b/packages/shared-ux/router/impl/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "stripInternal": false, + "types": [ + "jest", + "node", + "react", + "@kbn/ambient-ui-types", + ] + }, + "include": [ + "**/*", + ] +} diff --git a/packages/shared-ux/router/impl/types.ts b/packages/shared-ux/router/impl/types.ts new file mode 100644 index 00000000000000..a76e8a87c4fe38 --- /dev/null +++ b/packages/shared-ux/router/impl/types.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ + +/** + * @public + * Represents a meta-information about a Kibana entity initiating a search request. + */ +export declare interface SharedUXExecutionContext { + /** + * Kibana application initiated an operation. + * */ + readonly type?: string; + /** public name of an application or a user-facing feature */ + readonly name?: string; + /** a stand alone, logical unit such as an application page or tab */ + readonly page?: string; + /** unique value to identify the source */ + readonly id?: string; + /** human readable description. For example, a vis title, action name */ + readonly description?: string; + /** in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url */ + readonly url?: string; + /** Metadata attached to the field. An optional parameter that allows to describe the execution context in more detail. **/ + readonly meta?: { + [key: string]: string | number | boolean | undefined; + }; + /** an inner context spawned from the current context. */ + child?: SharedUXExecutionContext; +} diff --git a/packages/shared-ux/router/impl/use_execution_context.ts b/packages/shared-ux/router/impl/use_execution_context.ts new file mode 100644 index 00000000000000..e2bb6168d12686 --- /dev/null +++ b/packages/shared-ux/router/impl/use_execution_context.ts @@ -0,0 +1,29 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import useDeepCompareEffect from 'react-use/lib/useDeepCompareEffect'; +import { SharedUXExecutionContextSetup } from './services'; +import { SharedUXExecutionContext } from './types'; + +/** + * Set and clean up application level execution context + * @param executionContext + * @param context + */ +export function useSharedUXExecutionContext( + executionContext: SharedUXExecutionContextSetup | undefined, + context: SharedUXExecutionContext +) { + useDeepCompareEffect(() => { + executionContext?.set(context); + + return () => { + executionContext?.clear(); + }; + }, [context]); +} diff --git a/packages/shared-ux/router/mocks/BUILD.bazel b/packages/shared-ux/router/mocks/BUILD.bazel new file mode 100644 index 00000000000000..248dd93ce803ba --- /dev/null +++ b/packages/shared-ux/router/mocks/BUILD.bazel @@ -0,0 +1,135 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "mocks" +PKG_REQUIRE_NAME = "@kbn/shared-ux-router-mocks" + +SOURCE_FILES = glob( + [ + "**/*.ts", + "**/*.tsx", + ], + exclude = [ + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__", + "**/integration_tests", + "**/mocks", + "**/scripts", + "**/storybook", + "**/test_fixtures", + "**/test_helpers", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "@npm//react" +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "@npm//@types/react" +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + declaration_map = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = ".", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/shared-ux/router/mocks/README.md b/packages/shared-ux/router/mocks/README.md new file mode 100644 index 00000000000000..4aa41535f4bb20 --- /dev/null +++ b/packages/shared-ux/router/mocks/README.md @@ -0,0 +1,3 @@ +# @kbn/shared-ux-router-mocks + +Empty package generated by @kbn/generate diff --git a/packages/shared-ux/router/mocks/index.ts b/packages/shared-ux/router/mocks/index.ts new file mode 100644 index 00000000000000..b6e7485e36ab2c --- /dev/null +++ b/packages/shared-ux/router/mocks/index.ts @@ -0,0 +1,11 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +export function foo() { + return 'hello world'; +} diff --git a/packages/shared-ux/router/mocks/jest.config.js b/packages/shared-ux/router/mocks/jest.config.js new file mode 100644 index 00000000000000..9fbc3e5c702467 --- /dev/null +++ b/packages/shared-ux/router/mocks/jest.config.js @@ -0,0 +1,13 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/packages/shared-ux/router/mocks'], +}; diff --git a/packages/shared-ux/router/mocks/package.json b/packages/shared-ux/router/mocks/package.json new file mode 100644 index 00000000000000..d089a5d01f1062 --- /dev/null +++ b/packages/shared-ux/router/mocks/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/shared-ux-router-mocks", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/shared-ux/router/mocks/src/index.ts b/packages/shared-ux/router/mocks/src/index.ts new file mode 100644 index 00000000000000..4687a8e2cb53f4 --- /dev/null +++ b/packages/shared-ux/router/mocks/src/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ + +export { RouterMock } from './storybook'; +export type { RouterParams } from './storybook'; diff --git a/packages/shared-ux/router/mocks/src/storybook.ts b/packages/shared-ux/router/mocks/src/storybook.ts new file mode 100644 index 00000000000000..96c15d715cdee4 --- /dev/null +++ b/packages/shared-ux/router/mocks/src/storybook.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 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 or the Server + * Side Public License, v 1. + */ + +export const RouterMock = undefined; +export type RouterParams = undefined; diff --git a/packages/shared-ux/router/mocks/tsconfig.json b/packages/shared-ux/router/mocks/tsconfig.json new file mode 100644 index 00000000000000..a4f1ce7985a55b --- /dev/null +++ b/packages/shared-ux/router/mocks/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": ".", + "stripInternal": false, + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ] +} diff --git a/packages/shared-ux/router/types/BUILD.bazel b/packages/shared-ux/router/types/BUILD.bazel new file mode 100644 index 00000000000000..b33071f126efe8 --- /dev/null +++ b/packages/shared-ux/router/types/BUILD.bazel @@ -0,0 +1,60 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "types" +PKG_REQUIRE_NAME = "@kbn/shared-ux-router-types" + +SRCS = glob( + [ + "*.d.ts", + ] +) + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ +] + +js_library( + name = PKG_DIRNAME, + srcs = SRCS + NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +alias( + name = "npm_module_types", + actual = ":" + PKG_DIRNAME, + visibility = ["//visibility:public"], +) + diff --git a/packages/shared-ux/router/types/README.md b/packages/shared-ux/router/types/README.md new file mode 100644 index 00000000000000..ad806d7d070bd3 --- /dev/null +++ b/packages/shared-ux/router/types/README.md @@ -0,0 +1,3 @@ +# @kbn/shared-ux-router-types + +TODO: rshen91 diff --git a/packages/shared-ux/router/types/index.d.ts b/packages/shared-ux/router/types/index.d.ts new file mode 100644 index 00000000000000..5c2d5b68ae2e03 --- /dev/null +++ b/packages/shared-ux/router/types/index.d.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ diff --git a/packages/shared-ux/router/types/package.json b/packages/shared-ux/router/types/package.json new file mode 100644 index 00000000000000..323e9848a50a7e --- /dev/null +++ b/packages/shared-ux/router/types/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/shared-ux-router-types", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/shared-ux/router/types/tsconfig.json b/packages/shared-ux/router/types/tsconfig.json new file mode 100644 index 00000000000000..1a57218f76493d --- /dev/null +++ b/packages/shared-ux/router/types/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "stripInternal": false, + "types": [] + }, + "include": [ + "*.d.ts" + ] +} diff --git a/src/core/public/core_system.test.mocks.ts b/src/core/public/core_system.test.mocks.ts index 2b378b02554cb0..f5b0c017dcd188 100644 --- a/src/core/public/core_system.test.mocks.ts +++ b/src/core/public/core_system.test.mocks.ts @@ -21,7 +21,7 @@ import { pluginsServiceMock } from './plugins/plugins_service.mock'; import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks'; import { renderingServiceMock } from '@kbn/core-rendering-browser-mocks'; import { integrationsServiceMock } from '@kbn/core-integrations-browser-mocks'; -import { coreAppMock } from './core_app/core_app.mock'; +import { coreAppsMock } from '@kbn/core-apps-browser-mocks'; export const analyticsServiceStartMock = analyticsServiceMock.createAnalyticsServiceStart(); export const MockAnalyticsService = analyticsServiceMock.create(); @@ -126,10 +126,10 @@ jest.doMock('@kbn/core-integrations-browser-internal', () => ({ IntegrationsService: IntegrationsServiceConstructor, })); -export const MockCoreApp = coreAppMock.create(); +export const MockCoreApp = coreAppsMock.create(); export const CoreAppConstructor = jest.fn().mockImplementation(() => MockCoreApp); -jest.doMock('./core_app', () => ({ - CoreApp: CoreAppConstructor, +jest.doMock('@kbn/core-apps-browser-internal', () => ({ + CoreAppsService: CoreAppConstructor, })); export const MockThemeService = themeServiceMock.create(); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index cf3a9e6405f690..47ea6dd2ec164f 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -38,10 +38,10 @@ import { type InternalApplicationStart, } from '@kbn/core-application-browser-internal'; import { RenderingService } from '@kbn/core-rendering-browser-internal'; +import { CoreAppsService } from '@kbn/core-apps-browser-internal'; import { fetchOptionalMemoryInfo } from './fetch_optional_memory_info'; import { CoreSetup, CoreStart } from '.'; import { PluginsService } from './plugins'; -import { CoreApp } from './core_app'; import { LOAD_SETUP_DONE, @@ -95,7 +95,7 @@ export class CoreSystem { private readonly docLinks: DocLinksService; private readonly rendering: RenderingService; private readonly integrations: IntegrationsService; - private readonly coreApp: CoreApp; + private readonly coreApp: CoreAppsService; private readonly deprecations: DeprecationsService; private readonly theme: ThemeService; private readonly rootDomElement: HTMLElement; @@ -140,7 +140,7 @@ export class CoreSystem { this.executionContext = new ExecutionContextService(); this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins); - this.coreApp = new CoreApp(this.coreContext); + this.coreApp = new CoreAppsService(this.coreContext); performance.mark(KBN_LOAD_MARKS, { detail: LOAD_CORE_CREATED, diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 9eba054ab53020..e5c137a6d5db4b 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -224,7 +224,7 @@ export type { export type { MountPoint, UnmountCallback, OverlayRef } from '@kbn/core-mount-utils-browser'; -export { URL_MAX_LENGTH } from './core_app'; +export { URL_MAX_LENGTH } from '@kbn/core-apps-browser-internal'; export type { KibanaExecutionContext } from '@kbn/core-execution-context-common'; diff --git a/src/core/public/styles/core_app/README.txt b/src/core/public/styles/core_app/README.txt new file mode 100644 index 00000000000000..06f79f0f1afc1e --- /dev/null +++ b/src/core/public/styles/core_app/README.txt @@ -0,0 +1 @@ +These files are only used by the sass loader, located here: `packages/kbn-optimizer/src/worker/webpack.config.ts` diff --git a/src/core/public/core_app/styles/_globals_v8dark.scss b/src/core/public/styles/core_app/_globals_v8dark.scss similarity index 100% rename from src/core/public/core_app/styles/_globals_v8dark.scss rename to src/core/public/styles/core_app/_globals_v8dark.scss diff --git a/src/core/public/core_app/styles/_globals_v8light.scss b/src/core/public/styles/core_app/_globals_v8light.scss similarity index 100% rename from src/core/public/core_app/styles/_globals_v8light.scss rename to src/core/public/styles/core_app/_globals_v8light.scss diff --git a/src/core/public/core_app/styles/_mixins.scss b/src/core/public/styles/core_app/_mixins.scss similarity index 100% rename from src/core/public/core_app/styles/_mixins.scss rename to src/core/public/styles/core_app/_mixins.scss diff --git a/src/core/public/core_app/images/bg_bottom_branded.svg b/src/core/public/styles/core_app/images/bg_bottom_branded.svg similarity index 100% rename from src/core/public/core_app/images/bg_bottom_branded.svg rename to src/core/public/styles/core_app/images/bg_bottom_branded.svg diff --git a/src/core/public/core_app/images/bg_bottom_branded_dark.svg b/src/core/public/styles/core_app/images/bg_bottom_branded_dark.svg similarity index 100% rename from src/core/public/core_app/images/bg_bottom_branded_dark.svg rename to src/core/public/styles/core_app/images/bg_bottom_branded_dark.svg diff --git a/src/core/public/core_app/images/bg_top_branded.svg b/src/core/public/styles/core_app/images/bg_top_branded.svg similarity index 100% rename from src/core/public/core_app/images/bg_top_branded.svg rename to src/core/public/styles/core_app/images/bg_top_branded.svg diff --git a/src/core/public/core_app/images/bg_top_branded_dark.svg b/src/core/public/styles/core_app/images/bg_top_branded_dark.svg similarity index 100% rename from src/core/public/core_app/images/bg_top_branded_dark.svg rename to src/core/public/styles/core_app/images/bg_top_branded_dark.svg diff --git a/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx b/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx index 035faf8d22b003..7cf772ff7dd542 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx @@ -33,6 +33,7 @@ import { DatatablesWithFormatInfo, LayersAccessorsTitles, LayersFieldFormats, + hasMultipleLayersWithSplits, } from '../helpers'; interface Props { @@ -108,6 +109,7 @@ export const DataLayers: FC = ({ { commonLayerId: formattedDatatables[layers[0].layerId] } ) : getColorAssignments(layers, titles, fieldFormats, formattedDatatables); + const multipleLayersWithSplits = hasMultipleLayersWithSplits(layers); return ( <> {layers.flatMap((layer) => { @@ -163,6 +165,7 @@ export const DataLayers: FC = ({ uiState, allYAccessors, singleTable, + multipleLayersWithSplits, }); const index = `${layer.layerId}-${accessorIndex}`; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/legend_action.tsx b/src/plugins/chart_expressions/expression_xy/public/components/legend_action.tsx index 0d1d3b5b592569..37789a6ffa15e3 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/legend_action.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/legend_action.tsx @@ -15,6 +15,7 @@ import { LegendActionPopover } from './legend_action_popover'; import { DatatablesWithFormatInfo, getSeriesName, + hasMultipleLayersWithSplits, LayersAccessorsTitles, LayersFieldFormats, } from '../helpers'; @@ -85,6 +86,7 @@ export const getLegendAction = ( splitAccessorsFormats: fieldFormats[layer.layerId].splitSeriesAccessors, alreadyFormattedColumns: formattedDatatables[layer.layerId].formattedColumns, columnToLabelMap: layer.columnToLabel ? JSON.parse(layer.columnToLabel) : {}, + multipleLayersWithSplits: hasMultipleLayersWithSplits(dataLayers), }, titles )?.toString() || '' diff --git a/src/plugins/chart_expressions/expression_xy/public/components/legend_color_picker.tsx b/src/plugins/chart_expressions/expression_xy/public/components/legend_color_picker.tsx index 3573fc65e52884..7ae16b9c6f05a1 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/legend_color_picker.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/legend_color_picker.tsx @@ -15,6 +15,7 @@ import { DatatablesWithFormatInfo, getMetaFromSeriesId, getSeriesName, + hasMultipleLayersWithSplits, LayersAccessorsTitles, LayersFieldFormats, } from '../helpers'; @@ -90,6 +91,7 @@ export const LegendColorPickerWrapper: LegendColorPicker = ({ splitAccessorsFormats: fieldFormats[layer.layerId].splitSeriesAccessors, alreadyFormattedColumns: formattedDatatables[layer.layerId].formattedColumns, columnToLabelMap: layer.columnToLabel ? JSON.parse(layer.columnToLabel) : {}, + multipleLayersWithSplits: hasMultipleLayersWithSplits(dataLayers), }, titles[layer.layerId] )?.toString() || '' diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts index 2cce918d4b7984..94b187055e6dd7 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts @@ -19,7 +19,11 @@ import { LayersAccessorsTitles, LayersFieldFormats, } from './layers'; -import { DatatablesWithFormatInfo, DatatableWithFormatInfo } from './data_layers'; +import { + DatatablesWithFormatInfo, + DatatableWithFormatInfo, + hasMultipleLayersWithSplits, +} from './data_layers'; export const defaultReferenceLineColor = euiLightVars.euiColorDarkShade; @@ -59,7 +63,8 @@ export const getAllSeries = ( columnToLabel: CommonXYDataLayerConfig['columnToLabel'], titles: LayerAccessorsTitles, fieldFormats: LayerFieldFormats, - accessorsCount: number + accessorsCount: number, + multipleLayersWithSplits: boolean ) => { if (!formattedDatatable.table) { return []; @@ -77,7 +82,8 @@ export const getAllSeries = ( const yTitle = columnToLabelMap[yAccessor] ?? titles?.yTitles?.[yAccessor] ?? null; let name = yTitle; if (splitName) { - name = accessorsCount > 1 ? `${splitName} - ${yTitle}` : splitName; + name = + accessorsCount > 1 || multipleLayersWithSplits ? `${splitName} - ${yTitle}` : splitName; } if (!allSeries.includes(name)) { @@ -108,6 +114,7 @@ export function getColorAssignments( } layersPerPalette[palette].push(layer); }); + const multipleLayersWithSplits = hasMultipleLayersWithSplits(layers); return mapValues(layersPerPalette, (paletteLayers) => { const seriesPerLayer = paletteLayers.map((layer) => { @@ -119,7 +126,8 @@ export function getColorAssignments( layer.columnToLabel, titles[layer.layerId], fieldFormats[layer.layerId], - layer.accessors.length + layer.accessors.length, + multipleLayersWithSplits ) || []; return { numberOfSeries: allSeries.length, allSeries }; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx index 8098bb0efe02b2..7476d43f773e8b 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx @@ -24,7 +24,8 @@ import { Datatable } from '@kbn/expressions-plugin/common'; import { getAccessorByDimension } from '@kbn/visualizations-plugin/common/utils'; import type { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common/expression_functions'; import { PaletteRegistry, SeriesLayer } from '@kbn/coloring'; -import { CommonXYDataLayerConfig, XScaleType } from '../../common'; +import { isDataLayer } from '../../common/utils/layer_types_guards'; +import { CommonXYDataLayerConfig, CommonXYLayerConfig, XScaleType } from '../../common'; import { AxisModes, SeriesTypes } from '../../common/constants'; import { FormatFactory } from '../types'; import { getSeriesColor } from './state'; @@ -55,6 +56,7 @@ type GetSeriesPropsFn = (config: { uiState?: PersistedState; allYAccessors: Array; singleTable?: boolean; + multipleLayersWithSplits: boolean; }) => SeriesSpec; type GetSeriesNameFn = ( @@ -66,6 +68,7 @@ type GetSeriesNameFn = ( splitAccessorsFormats: LayerFieldFormats['splitSeriesAccessors']; alreadyFormattedColumns: Record; columnToLabelMap: Record; + multipleLayersWithSplits: boolean; }, titles: LayerAccessorsTitles ) => SeriesName; @@ -254,6 +257,7 @@ export const getSeriesName: GetSeriesNameFn = ( splitAccessorsFormats, alreadyFormattedColumns, columnToLabelMap, + multipleLayersWithSplits, }, titles ) => { @@ -272,7 +276,7 @@ export const getSeriesName: GetSeriesNameFn = ( const key = data.seriesKeys[data.seriesKeys.length - 1]; const yAccessorTitle = columnToLabelMap[key] ?? titles?.yTitles?.[key] ?? null; - if (accessorsCount > 1) { + if (accessorsCount > 1 || multipleLayersWithSplits) { if (splitValues.length === 0) { return yAccessorTitle; } @@ -369,6 +373,10 @@ export const getMetaFromSeriesId = (seriesId: string) => { }; }; +export function hasMultipleLayersWithSplits(layers: CommonXYLayerConfig[]) { + return layers.filter((l) => isDataLayer(l) && (l.splitAccessors?.length || 0) > 0).length > 1; +} + export const getSeriesProps: GetSeriesPropsFn = ({ layer, titles = {}, @@ -389,6 +397,7 @@ export const getSeriesProps: GetSeriesPropsFn = ({ uiState, allYAccessors, singleTable, + multipleLayersWithSplits, }): SeriesSpec => { const { table, isStacked, markSizeAccessor } = layer; const isPercentage = layer.isPercentage; @@ -464,6 +473,7 @@ export const getSeriesProps: GetSeriesPropsFn = ({ columns: formattedTable.columns, splitAccessorsFormats: fieldFormats[layer.layerId].splitSeriesAccessors, columnToLabelMap, + multipleLayersWithSplits, }, titles ); diff --git a/src/plugins/console/public/application/components/console_menu.tsx b/src/plugins/console/public/application/components/console_menu.tsx index 159c0d600308f3..3f5113b3ac44f6 100644 --- a/src/plugins/console/public/application/components/console_menu.tsx +++ b/src/plugins/console/public/application/components/console_menu.tsx @@ -31,6 +31,7 @@ interface Props { interface State { isPopoverOpen: boolean; curlCode: string; + curlError: Error | null; } export class ConsoleMenu extends Component { @@ -40,14 +41,20 @@ export class ConsoleMenu extends Component { this.state = { curlCode: '', isPopoverOpen: false, + curlError: null, }; } mouseEnter = () => { if (this.state.isPopoverOpen) return; - this.props.getCurl().then((text) => { - this.setState({ curlCode: text }); - }); + this.props + .getCurl() + .then((text) => { + this.setState({ curlCode: text, curlError: null }); + }) + .catch((e) => { + this.setState({ curlError: e }); + }); }; async copyAsCurl() { @@ -69,6 +76,9 @@ export class ConsoleMenu extends Component { } async copyText(text: string) { + if (this.state.curlError) { + throw this.state.curlError; + } if (window.navigator?.clipboard) { await window.navigator.clipboard.writeText(text); return; diff --git a/src/plugins/console/public/application/index.tsx b/src/plugins/console/public/application/index.tsx index e9f37c232eeaa8..1cf9a54210973b 100644 --- a/src/plugins/console/public/application/index.tsx +++ b/src/plugins/console/public/application/index.tsx @@ -19,7 +19,13 @@ import { import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { KibanaThemeProvider } from '../shared_imports'; -import { createStorage, createHistory, createSettings, AutocompleteInfo } from '../services'; +import { + createStorage, + createHistory, + createSettings, + AutocompleteInfo, + setStorage, +} from '../services'; import { createUsageTracker } from '../services/tracker'; import * as localStorageObjectClient from '../lib/local_storage_object_client'; import { Main } from './containers'; @@ -56,6 +62,7 @@ export function renderApp({ engine: window.localStorage, prefix: 'sense:', }); + setStorage(storage); const history = createHistory({ storage }); const settings = createSettings({ storage }); const objectStorageClient = localStorageObjectClient.create(storage); diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js index 4751d3ca29863e..c6f484e8f16972 100644 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js @@ -15,6 +15,7 @@ import { URL } from 'url'; import { create } from './create'; import { XJson } from '@kbn/es-ui-shared-plugin/public'; import editorInput1 from './__fixtures__/editor_input1.txt'; +import { setStorage, createStorage } from '../../../services'; const { collapseLiteralStrings } = XJson; @@ -22,6 +23,7 @@ describe('Editor', () => { let input; let oldUrl; let olldWindow; + let storage; beforeEach(function () { // Set up our document body @@ -43,12 +45,18 @@ describe('Editor', () => { origin: 'http://localhost:5620', }, }); + storage = createStorage({ + engine: global.window.localStorage, + prefix: 'console_test', + }); + setStorage(storage); }); afterEach(function () { global.URL = oldUrl; global.window = olldWindow; $(input.getCoreEditor().getContainer()).hide(); input.autocomplete._test.addChangeListener(); + setStorage(null); }); let testCount = 0; @@ -506,4 +514,104 @@ curl -XPOST "http://localhost:9200/_sql?format=txt" -H "kbn-xsrf: reporting" -H ` curl -XGET "http://localhost:5620/api/spaces/space" -H \"kbn-xsrf: reporting\"`.trim() ); + + describe('getRequestsAsCURL', () => { + it('should return empty string if no requests', async () => { + input?.getCoreEditor().setValue('', false); + const curl = await input.getRequestsAsCURL('http://localhost:9200', { + start: { lineNumber: 1 }, + end: { lineNumber: 1 }, + }); + expect(curl).toEqual(''); + }); + + it('should replace variables in the URL', async () => { + storage.set('variables', [{ name: 'exampleVariableA', value: 'valueA' }]); + input?.getCoreEditor().setValue('GET ${exampleVariableA}', false); + const curl = await input.getRequestsAsCURL('http://localhost:9200', { + start: { lineNumber: 1 }, + end: { lineNumber: 1 }, + }); + expect(curl).toContain('valueA'); + }); + + it('should replace variables in the body', async () => { + storage.set('variables', [{ name: 'exampleVariableB', value: 'valueB' }]); + console.log(storage.get('variables')); + input + ?.getCoreEditor() + .setValue('GET _search\n{\t\t"query": {\n\t\t\t"${exampleVariableB}": ""\n\t}\n}', false); + const curl = await input.getRequestsAsCURL('http://localhost:9200', { + start: { lineNumber: 1 }, + end: { lineNumber: 6 }, + }); + expect(curl).toContain('valueB'); + }); + + it('should strip comments in the URL', async () => { + input?.getCoreEditor().setValue('GET _search // comment', false); + const curl = await input.getRequestsAsCURL('http://localhost:9200', { + start: { lineNumber: 1 }, + end: { lineNumber: 6 }, + }); + expect(curl).not.toContain('comment'); + }); + + it('should strip comments in the body', async () => { + input + ?.getCoreEditor() + .setValue('{\n\t"query": {\n\t\t"match_all": {} // comment \n\t}\n}', false); + const curl = await input.getRequestsAsCURL('http://localhost:9200', { + start: { lineNumber: 1 }, + end: { lineNumber: 8 }, + }); + console.log('curl', curl); + expect(curl).not.toContain('comment'); + }); + + it('should strip multi-line comments in the body', async () => { + input + ?.getCoreEditor() + .setValue('{\n\t"query": {\n\t\t"match_all": {} /* comment */\n\t}\n}', false); + const curl = await input.getRequestsAsCURL('http://localhost:9200', { + start: { lineNumber: 1 }, + end: { lineNumber: 8 }, + }); + console.log('curl', curl); + expect(curl).not.toContain('comment'); + }); + + it('should replace multiple variables in the URL', async () => { + storage.set('variables', [ + { name: 'exampleVariableA', value: 'valueA' }, + { name: 'exampleVariableB', value: 'valueB' }, + ]); + input?.getCoreEditor().setValue('GET ${exampleVariableA}/${exampleVariableB}', false); + const curl = await input.getRequestsAsCURL('http://localhost:9200', { + start: { lineNumber: 1 }, + end: { lineNumber: 1 }, + }); + expect(curl).toContain('valueA'); + expect(curl).toContain('valueB'); + }); + + it('should replace multiple variables in the body', async () => { + storage.set('variables', [ + { name: 'exampleVariableA', value: 'valueA' }, + { name: 'exampleVariableB', value: 'valueB' }, + ]); + input + ?.getCoreEditor() + .setValue( + 'GET _search\n{\t\t"query": {\n\t\t\t"${exampleVariableA}": "${exampleVariableB}"\n\t}\n}', + false + ); + const curl = await input.getRequestsAsCURL('http://localhost:9200', { + start: { lineNumber: 1 }, + end: { lineNumber: 6 }, + }); + expect(curl).toContain('valueA'); + expect(curl).toContain('valueB'); + }); + }); }); diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts index 50ee1cd1d262a1..ac2205205ab465 100644 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts @@ -7,7 +7,7 @@ */ import _ from 'lodash'; - +import { parse } from 'hjson'; import { XJson } from '@kbn/es-ui-shared-plugin/public'; import RowParser from '../../../lib/row_parser'; @@ -19,6 +19,8 @@ import { constructUrl } from '../../../lib/es/es'; import { CoreEditor, Position, Range } from '../../../types'; import { createTokenIterator } from '../../factories'; import createAutocompleter from '../../../lib/autocomplete/autocomplete'; +import { getStorage, StorageKeys } from '../../../services'; +import { DEFAULT_VARIABLES } from '../../../../common/constants'; const { collapseLiteralStrings } = XJson; @@ -474,7 +476,9 @@ export class SenseEditor { }, 25); getRequestsAsCURL = async (elasticsearchBaseUrl: string, range?: Range): Promise => { - const requests = await this.getRequestsInRange(range, true); + const variables = getStorage().get(StorageKeys.VARIABLES, DEFAULT_VARIABLES); + let requests = await this.getRequestsInRange(range, true); + requests = utils.replaceVariables(requests, variables); const result = _.map(requests, (req) => { if (typeof req === 'string') { // no request block @@ -490,16 +494,30 @@ export class SenseEditor { // Append 'kbn-xsrf' header to bypass (XSRF/CSRF) protections let ret = `curl -X${method.toUpperCase()} "${url}" -H "kbn-xsrf: reporting"`; + if (data && data.length) { - ret += ` -H "Content-Type: application/json" -d'\n`; - const dataAsString = collapseLiteralStrings(data.join('\n')); - - // We escape single quoted strings that that are wrapped in single quoted strings - ret += dataAsString.replace(/'/g, "'\\''"); - if (data.length > 1) { - ret += '\n'; - } // end with a new line - ret += "'"; + const joinedData = data.join('\n'); + let dataAsString: string; + + try { + ret += ` -H "Content-Type: application/json" -d'\n`; + + if (utils.hasComments(joinedData)) { + // if there are comments in the data, we need to strip them out + const dataWithoutComments = parse(joinedData); + dataAsString = collapseLiteralStrings(JSON.stringify(dataWithoutComments, null, 2)); + } else { + dataAsString = collapseLiteralStrings(joinedData); + } + // We escape single quoted strings that are wrapped in single quoted strings + ret += dataAsString.replace(/'/g, "'\\''"); + if (data.length > 1) { + ret += '\n'; + } // end with a new line + ret += "'"; + } catch (e) { + throw new Error(`Error parsing data: ${e.message}`); + } } return ret; }); diff --git a/src/plugins/console/public/lib/utils/index.ts b/src/plugins/console/public/lib/utils/index.ts index 2495f63c7614f3..dfa513085019d8 100644 --- a/src/plugins/console/public/lib/utils/index.ts +++ b/src/plugins/console/public/lib/utils/index.ts @@ -130,7 +130,7 @@ export const replaceVariables = ( }); } - if (req.data.length) { + if (req.data && req.data.length) { if (bodyRegex.test(req.data[0])) { const data = req.data[0].replaceAll(bodyRegex, (match) => { // Sanitize variable name diff --git a/src/plugins/console/public/services/index.ts b/src/plugins/console/public/services/index.ts index 2447ab1438ba4f..d73c169fd647a5 100644 --- a/src/plugins/console/public/services/index.ts +++ b/src/plugins/console/public/services/index.ts @@ -7,7 +7,7 @@ */ export { createHistory, History } from './history'; -export { createStorage, Storage, StorageKeys } from './storage'; +export { createStorage, Storage, StorageKeys, setStorage, getStorage } from './storage'; export type { DevToolsSettings } from './settings'; export { createSettings, Settings, DEFAULT_SETTINGS } from './settings'; export { AutocompleteInfo, getAutocompleteInfo, setAutocompleteInfo } from './autocomplete'; diff --git a/src/plugins/console/public/services/storage.ts b/src/plugins/console/public/services/storage.ts index 4b4d051607b8dd..b38cc2925dfb1d 100644 --- a/src/plugins/console/public/services/storage.ts +++ b/src/plugins/console/public/services/storage.ts @@ -7,6 +7,7 @@ */ import { transform, keys, startsWith } from 'lodash'; +import { createGetterSetter } from '@kbn/kibana-utils-plugin/public'; type IStorageEngine = typeof window.localStorage; @@ -71,3 +72,5 @@ export class Storage { export function createStorage(deps: { engine: IStorageEngine; prefix: string }) { return new Storage(deps.engine, deps.prefix); } + +export const [getStorage, setStorage] = createGetterSetter('storage'); diff --git a/src/plugins/console/server/lib/spec_definitions/js/aggregations.ts b/src/plugins/console/server/lib/spec_definitions/js/aggregations.ts index 9403d6d56e4860..16e7a77366c7dd 100644 --- a/src/plugins/console/server/lib/spec_definitions/js/aggregations.ts +++ b/src/plugins/console/server/lib/spec_definitions/js/aggregations.ts @@ -301,6 +301,9 @@ const rules = { format: 'yyyy-MM-dd', time_zone: '00:00', missing: '', + calendar_interval: { + __one_of: ['year', 'quarter', 'week', 'day', 'hour', 'minute', 'second'], + }, }, geo_distance: { __template: { @@ -473,7 +476,7 @@ const rules = { percents: [], }, sum_bucket: simple_pipeline, - moving_avg: { + moving_fn: { __template: { buckets_path: '', }, @@ -489,6 +492,7 @@ const rules = { gamma: 0.5, period: 7, }, + script: '', }, cumulative_sum: { __template: { diff --git a/src/plugins/console/server/lib/spec_definitions/js/aliases.ts b/src/plugins/console/server/lib/spec_definitions/js/aliases.ts index 9eb7554fabeba4..ef4a30c1aa012e 100644 --- a/src/plugins/console/server/lib/spec_definitions/js/aliases.ts +++ b/src/plugins/console/server/lib/spec_definitions/js/aliases.ts @@ -14,6 +14,8 @@ export const aliases = (specService: SpecDefinitionsService) => { routing: '1', search_routing: '1,2', index_routing: '1', + is_write_index: false, + is_hidden: false, }; specService.addGlobalAutocompleteRules('aliases', { '*': aliasRules, diff --git a/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_index_template.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_index_template.json new file mode 100644 index 00000000000000..95d3eabc97c36e --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_index_template.json @@ -0,0 +1,31 @@ +{ + "indices.put_index_template": { + "data_autocomplete_rules": { + "composed_of": [], + "index_patterns": [], + "data_stream": { + "__template": { + "allow_custom_routing": false, + "hidden": false, + "index_mode": "" + } + }, + "template": { + "settings": { + "__scope_link": "put_settings" + }, + "aliases": { + "__template": { + "NAME": {} + } + }, + "mappings": { + "__scope_link": "put_mapping" + } + }, + "_meta": {}, + "priority": 0, + "version": 0 + } + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/overrides/slm.put_lifecycle.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/slm.put_lifecycle.json index f14fa5a4ad634e..ae653df104b511 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/overrides/slm.put_lifecycle.json +++ b/src/plugins/console/server/lib/spec_definitions/json/overrides/slm.put_lifecycle.json @@ -4,7 +4,42 @@ "schedule": "", "name": "", "repository": "", - "config": {} + "config": { + "expanded_wildcards": { + "__one_of": [ + "open", + "closed", + "hidden", + "none", + "all" + ] + }, + "ignore_unavailable": false, + "include_global_state": false, + "indices": [ + "" + ], + "feature_states": [ + "" + ], + "partial": false, + "metadata": {} + }, + "retention": { + "expire_after": { + "__one_of": [ + "d", + "h", + "m", + "s", + "ms", + "micros", + "nanos" + ] + }, + "min_count": 0, + "max_count": 0 + } } } } diff --git a/src/plugins/data/common/es_query/stubs/phrase_filter.ts b/src/plugins/data/common/es_query/stubs/phrase_filter.ts index 8c951b4d5d1fc5..ef15d14750f9e6 100644 --- a/src/plugins/data/common/es_query/stubs/phrase_filter.ts +++ b/src/plugins/data/common/es_query/stubs/phrase_filter.ts @@ -24,5 +24,9 @@ export const phraseFilter: PhraseFilter = { $state: { store: FilterStateStore.APP_STATE, }, - query: {}, + query: { + match_phrase: { + 'machine.os': 'ios', + }, + }, }; diff --git a/src/plugins/data/common/es_query/stubs/range_filter.ts b/src/plugins/data/common/es_query/stubs/range_filter.ts index 26c26afe8f545c..a3799588b19f12 100644 --- a/src/plugins/data/common/es_query/stubs/range_filter.ts +++ b/src/plugins/data/common/es_query/stubs/range_filter.ts @@ -25,5 +25,5 @@ export const rangeFilter: RangeFilter = { $state: { store: FilterStateStore.APP_STATE, }, - query: { range: {} }, + query: { range: { bytes: { gt: 0, lt: 10 } } }, }; diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index 1a929df039ec06..f76b6b903fe95b 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -33,7 +33,6 @@ export type { SavedQuery, SavedQueryAttributes, SavedQueryTimeFilter, - FilterValueFormatter, KbnFieldTypeOptions, Query, } from './types'; diff --git a/src/plugins/data/common/search/aggs/utils/get_aggs_formats.test.ts b/src/plugins/data/common/search/aggs/utils/get_aggs_formats.test.ts index ea68be542f42f5..eb0d6df57b278d 100644 --- a/src/plugins/data/common/search/aggs/utils/get_aggs_formats.test.ts +++ b/src/plugins/data/common/search/aggs/utils/get_aggs_formats.test.ts @@ -42,7 +42,7 @@ describe('getAggsFormats', () => { ); expect(format.convert({ to: '2020-06-01' })).toBe('Before 2020-06-01'); expect(format.convert({ from: '2020-06-01' })).toBe('After 2020-06-01'); - expect(getFormat).toHaveBeenCalledTimes(3); + expect(getFormat).toHaveBeenCalledTimes(1); }); test('date_range does not crash on empty value', () => { @@ -62,7 +62,7 @@ describe('getAggsFormats', () => { expect(format.convert({ type: 'range', to: '10.0.0.10' })).toBe('-Infinity to 10.0.0.10'); expect(format.convert({ type: 'range', from: '10.0.0.10' })).toBe('10.0.0.10 to Infinity'); format.convert({ type: 'mask', mask: '10.0.0.1/24' }); - expect(getFormat).toHaveBeenCalledTimes(4); + expect(getFormat).toHaveBeenCalledTimes(1); }); test('ip_range does not crash on empty value', () => { @@ -135,7 +135,7 @@ describe('getAggsFormats', () => { expect(format.convert('machine.os.keyword')).toBe('machine.os.keyword'); expect(format.convert('__other__')).toBe(mapping.params.otherBucketLabel); expect(format.convert('__missing__')).toBe(mapping.params.missingBucketLabel); - expect(getFormat).toHaveBeenCalledTimes(3); + expect(getFormat).toHaveBeenCalledTimes(1); }); test('uses a default separator for multi terms', () => { diff --git a/src/plugins/data/common/search/aggs/utils/get_aggs_formats.ts b/src/plugins/data/common/search/aggs/utils/get_aggs_formats.ts index 5e36fbb791e28f..f9b8dd508d4a90 100644 --- a/src/plugins/data/common/search/aggs/utils/get_aggs_formats.ts +++ b/src/plugins/data/common/search/aggs/utils/get_aggs_formats.ts @@ -16,6 +16,7 @@ import { IFieldFormat, SerializedFieldFormat, } from '@kbn/field-formats-plugin/common'; +import { SerializableRecord } from '@kbn/utility-types'; import { DateRange } from '../../expressions'; import { convertDateRangeToString } from '../buckets/lib/date_range'; import { convertIPRangeToString, IpRangeKey } from '../buckets/lib/ip_range'; @@ -35,8 +36,21 @@ type GetFieldFormat = (mapping: SerializedFieldFormat) => IFieldFormat; * @internal */ export function getAggsFormats(getFieldFormat: GetFieldFormat): FieldFormatInstanceType[] { + class FieldFormatWithCache extends FieldFormat { + protected formatCache: Map = new Map(); + + protected getCachedFormat(fieldParams: SerializedFieldFormat<{}, SerializableRecord>) { + const isCached = this.formatCache.has(fieldParams); + const cachedFormat = this.formatCache.get(fieldParams) || getFieldFormat(fieldParams); + if (!isCached) { + this.formatCache.set(fieldParams, cachedFormat); + } + return cachedFormat; + } + } + return [ - class AggsRangeFieldFormat extends FieldFormat { + class AggsRangeFieldFormat extends FieldFormatWithCache { static id = 'range'; static hidden = true; @@ -51,10 +65,7 @@ export function getAggsFormats(getFieldFormat: GetFieldFormat): FieldFormatInsta return range.label; } const nestedFormatter = params as SerializedFieldFormat; - const format = getFieldFormat({ - id: nestedFormatter.id, - params: nestedFormatter.params, - }); + const format = this.getCachedFormat(nestedFormatter); const gte = '\u2265'; const lt = '\u003c'; @@ -88,7 +99,7 @@ export function getAggsFormats(getFieldFormat: GetFieldFormat): FieldFormatInsta }); }; }, - class AggsDateRangeFieldFormat extends FieldFormat { + class AggsDateRangeFieldFormat extends FieldFormatWithCache { static id = 'date_range'; static hidden = true; @@ -98,14 +109,11 @@ export function getAggsFormats(getFieldFormat: GetFieldFormat): FieldFormatInsta } const nestedFormatter = this._params as SerializedFieldFormat; - const format = getFieldFormat({ - id: nestedFormatter.id, - params: nestedFormatter.params, - }); + const format = this.getCachedFormat(nestedFormatter); return convertDateRangeToString(range, format.convert.bind(format)); }; }, - class AggsIpRangeFieldFormat extends FieldFormat { + class AggsIpRangeFieldFormat extends FieldFormatWithCache { static id = 'ip_range'; static hidden = true; @@ -115,20 +123,19 @@ export function getAggsFormats(getFieldFormat: GetFieldFormat): FieldFormatInsta } const nestedFormatter = this._params as SerializedFieldFormat; - const format = getFieldFormat({ - id: nestedFormatter.id, - params: nestedFormatter.params, - }); + const format = this.getCachedFormat(nestedFormatter); return convertIPRangeToString(range, format.convert.bind(format)); }; }, - class AggsTermsFieldFormat extends FieldFormat { + class AggsTermsFieldFormat extends FieldFormatWithCache { static id = 'terms'; static hidden = true; convert = (val: string, type: FieldFormatsContentType) => { const params = this._params; - const format = getFieldFormat({ id: `${params.id}`, params }); + const format = this.getCachedFormat( + params as SerializedFieldFormat<{}, SerializableRecord> + ); if (val === '__other__') { return `${params.otherBucketLabel}`; @@ -141,21 +148,14 @@ export function getAggsFormats(getFieldFormat: GetFieldFormat): FieldFormatInsta }; getConverterFor = (type: FieldFormatsContentType) => (val: string) => this.convert(val, type); }, - class AggsMultiTermsFieldFormat extends FieldFormat { + class AggsMultiTermsFieldFormat extends FieldFormatWithCache { static id = 'multi_terms'; static hidden = true; - private formatCache: Map = new Map(); - convert = (val: unknown, type: FieldFormatsContentType) => { const params = this._params; const formats = (params.paramsPerField as SerializedFieldFormat[]).map((fieldParams) => { - const isCached = this.formatCache.has(fieldParams); - const cachedFormat = this.formatCache.get(fieldParams) || getFieldFormat(fieldParams); - if (!isCached) { - this.formatCache.set(fieldParams, cachedFormat); - } - return cachedFormat; + return this.getCachedFormat(fieldParams); }); if (String(val) === '__other__') { diff --git a/src/plugins/data/common/search/strategies/eql_search/types.ts b/src/plugins/data/common/search/strategies/eql_search/types.ts index 7f6ec4809b2c5d..985dbb409e2056 100644 --- a/src/plugins/data/common/search/strategies/eql_search/types.ts +++ b/src/plugins/data/common/search/strategies/eql_search/types.ts @@ -16,6 +16,9 @@ export const EQL_SEARCH_STRATEGY = 'eql'; export type EqlRequestParams = EqlSearchRequest; export interface EqlSearchStrategyRequest extends IKibanaSearchRequest { + /** + * @deprecated: use IAsyncSearchOptions.transport instead. + */ options?: TransportRequestOptions; } diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts index fec48e2a54a0af..802af347b8e69a 100644 --- a/src/plugins/data/common/search/types.ts +++ b/src/plugins/data/common/search/types.ts @@ -5,6 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + +import type { TransportRequestOptions } from '@elastic/elasticsearch'; import type { KibanaExecutionContext } from '@kbn/core/public'; import type { DataView } from '@kbn/data-views-plugin/common'; import { Observable } from 'rxjs'; @@ -132,6 +134,12 @@ export interface ISearchOptions { * Index pattern reference is used for better error messages */ indexPattern?: DataView; + + /** + * TransportRequestOptions, other than `signal`, to pass through to the ES client. + * To pass an abort signal, use {@link ISearchOptions.abortSignal} + */ + transport?: Omit; } /** diff --git a/src/plugins/data/common/types.ts b/src/plugins/data/common/types.ts index 81b47735d8fe2e..84c8f8b5a3fe1e 100644 --- a/src/plugins/data/common/types.ts +++ b/src/plugins/data/common/types.ts @@ -20,9 +20,3 @@ export * from './kbn_field_types/types'; * not possible. */ export type GetConfigFn = (key: string, defaultOverride?: T) => T; - -type FilterFormatterFunction = (value: any) => string; -export interface FilterValueFormatter { - convert: FilterFormatterFunction; - getConverterFor: (type: string) => FilterFormatterFunction; -} diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index ed50d78f4de3c1..c8eefbdd92c6e0 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -291,7 +291,6 @@ export function plugin(initializerContext: PluginInitializerContext { }); it('returns the value if string', () => { - phraseFilter.meta.value = 'abc'; - const displayValue = getDisplayValueFromFilter(phraseFilter, [stubIndexPattern]); + const filter = { ...phraseFilter, meta: { ...phraseFilter.meta, value: 'abc' } }; + const displayValue = getDisplayValueFromFilter(filter, [stubIndexPattern]); expect(displayValue).toBe('abc'); }); it('returns the value if undefined', () => { - phraseFilter.meta.value = undefined; - const displayValue = getDisplayValueFromFilter(phraseFilter, [stubIndexPattern]); + const filter = { + ...phraseFilter, + meta: { ...phraseFilter.meta, value: undefined, params: { query: undefined } }, + }; + const displayValue = getDisplayValueFromFilter(filter, [stubIndexPattern]); expect(displayValue).toBe(''); }); - it('calls the value function if provided', () => { - // The type of value currently doesn't match how it's used. Refactor needed. - phraseFilter.meta.value = jest.fn((x) => { - return 'abc'; - }) as any; + it('phrase filters without formatter', () => { jest.spyOn(stubIndexPattern, 'getFormatterForField').mockImplementation(() => undefined!); const displayValue = getDisplayValueFromFilter(phraseFilter, [stubIndexPattern]); - expect(displayValue).toBe('abc'); - expect(phraseFilter.meta.value).toHaveBeenCalledWith(undefined); + expect(displayValue).toBe('ios'); }); - it('calls the value function if provided, with formatter', () => { + it('phrase filters with formatter', () => { const mockFormatter = new (FieldFormat.from((value: string) => 'banana' + value))(); jest.spyOn(stubIndexPattern, 'getFormatterForField').mockImplementation(() => mockFormatter); - phraseFilter.meta.value = jest.fn((x) => { - return x.convert('abc'); - }) as any; const displayValue = getDisplayValueFromFilter(phraseFilter, [stubIndexPattern]); - expect(stubIndexPattern.getFormatterForField).toHaveBeenCalledTimes(1); - expect(phraseFilter.meta.value).toHaveBeenCalledWith(mockFormatter); - expect(displayValue).toBe('bananaabc'); + expect(displayValue).toBe('bananaios'); + }); + + it('phrases filters without formatter', () => { + jest.spyOn(stubIndexPattern, 'getFormatterForField').mockImplementation(() => undefined!); + const displayValue = getDisplayValueFromFilter(phrasesFilter, [stubIndexPattern]); + expect(displayValue).toBe('win xp, osx'); + }); + + it('phrases filters with formatter', () => { + const mockFormatter = new (FieldFormat.from((value: string) => 'banana' + value))(); + jest.spyOn(stubIndexPattern, 'getFormatterForField').mockImplementation(() => mockFormatter); + const displayValue = getDisplayValueFromFilter(phrasesFilter, [stubIndexPattern]); + expect(displayValue).toBe('bananawin xp, bananaosx'); + }); + + it('range filters without formatter', () => { + jest.spyOn(stubIndexPattern, 'getFormatterForField').mockImplementation(() => undefined!); + const displayValue = getDisplayValueFromFilter(rangeFilter, [stubIndexPattern]); + expect(displayValue).toBe('0 to 10'); + }); + + it('range filters with formatter', () => { + const mockFormatter = new (FieldFormat.from((value: string) => 'banana' + value))(); + jest.spyOn(stubIndexPattern, 'getFormatterForField').mockImplementation(() => mockFormatter); + const displayValue = getDisplayValueFromFilter(rangeFilter, [stubIndexPattern]); + expect(displayValue).toBe('banana0 to banana10'); }); }); diff --git a/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts b/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts index 40bbe9a89c9926..5543e0071b4d09 100644 --- a/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts +++ b/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts @@ -8,7 +8,18 @@ import { i18n } from '@kbn/i18n'; import { DataView, DataViewField } from '@kbn/data-views-plugin/public'; -import { Filter } from '@kbn/es-query'; +import { + Filter, + isPhraseFilter, + isPhrasesFilter, + isRangeFilter, + isScriptedPhraseFilter, + isScriptedRangeFilter, + getFilterField, +} from '@kbn/es-query'; +import { getPhraseDisplayValue } from './mappers/map_phrase'; +import { getPhrasesDisplayValue } from './mappers/map_phrases'; +import { getRangeDisplayValue } from './mappers/map_range'; import { getIndexPatternFromFilter } from './get_index_pattern_from_filter'; function getValueFormatter(indexPattern?: DataView, key?: string) { @@ -29,22 +40,26 @@ function getValueFormatter(indexPattern?: DataView, key?: string) { } export function getFieldDisplayValueFromFilter(filter: Filter, indexPatterns: DataView[]): string { - const { key } = filter.meta; const indexPattern = getIndexPatternFromFilter(filter, indexPatterns); if (!indexPattern) return ''; - const field = indexPattern.fields.find((f: DataViewField) => f.name === key); + + const fieldName = getFilterField(filter); + if (!fieldName) return ''; + + const field = indexPattern.fields.find((f: DataViewField) => f.name === fieldName); return field?.customLabel ?? ''; } export function getDisplayValueFromFilter(filter: Filter, indexPatterns: DataView[]): string { - const { key, value } = filter.meta; - if (typeof value === 'function') { - const indexPattern = getIndexPatternFromFilter(filter, indexPatterns); - const valueFormatter = getValueFormatter(indexPattern, key); - // TODO: distinguish between FilterMeta which is serializable to mapped FilterMeta - // Where value can be a function. - return (value as any)(valueFormatter); - } else { - return value || ''; - } + const indexPattern = getIndexPatternFromFilter(filter, indexPatterns); + const fieldName = getFilterField(filter); + const valueFormatter = getValueFormatter(indexPattern, fieldName); + + if (isPhraseFilter(filter) || isScriptedPhraseFilter(filter)) { + return getPhraseDisplayValue(filter, valueFormatter); + } else if (isPhrasesFilter(filter)) { + return getPhrasesDisplayValue(filter, valueFormatter); + } else if (isRangeFilter(filter) || isScriptedRangeFilter(filter)) { + return getRangeDisplayValue(filter, valueFormatter); + } else return filter.meta.value ?? ''; } diff --git a/src/plugins/data/public/query/filter_manager/lib/map_and_flatten_filters.test.ts b/src/plugins/data/public/query/filter_manager/lib/map_and_flatten_filters.test.ts index e7b40678816562..91b2ae8d3ada62 100644 --- a/src/plugins/data/public/query/filter_manager/lib/map_and_flatten_filters.test.ts +++ b/src/plugins/data/public/query/filter_manager/lib/map_and_flatten_filters.test.ts @@ -13,12 +13,6 @@ describe('filter manager utilities', () => { describe('mapAndFlattenFilters()', () => { let filters: unknown; - function getDisplayName(filter: Filter) { - return typeof filter.meta.value === 'function' - ? (filter.meta.value as any)() - : filter.meta.value; - } - beforeEach(() => { filters = [ null, @@ -51,11 +45,8 @@ describe('filter manager utilities', () => { expect(results[2].meta).toHaveProperty('key', 'query'); expect(results[2].meta).toHaveProperty('value', 'foo:bar'); expect(results[3].meta).toHaveProperty('key', 'bytes'); - expect(results[3].meta).toHaveProperty('value'); - expect(getDisplayName(results[3])).toBe('1024 to 2048'); + expect(results[3].meta).toHaveProperty('value', { gt: 1024, lt: 2048 }); expect(results[4].meta).toHaveProperty('key', '_type'); - expect(results[4].meta).toHaveProperty('value'); - expect(getDisplayName(results[4])).toBe('apache'); }); }); }); diff --git a/src/plugins/data/public/query/filter_manager/lib/map_filter.test.ts b/src/plugins/data/public/query/filter_manager/lib/map_filter.test.ts index be94e69f74f9d2..ff9c6d47660fd8 100644 --- a/src/plugins/data/public/query/filter_manager/lib/map_filter.test.ts +++ b/src/plugins/data/public/query/filter_manager/lib/map_filter.test.ts @@ -7,27 +7,22 @@ */ import { mapFilter } from './map_filter'; -import type { Filter } from '@kbn/es-query'; +import type { Filter, PhraseFilter } from '@kbn/es-query'; +import { getDisplayValueFromFilter } from '../../..'; describe('filter manager utilities', () => { - function getDisplayName(filter: Filter) { - return typeof filter.meta.value === 'function' - ? (filter.meta.value as any)() - : filter.meta.value; - } - describe('mapFilter()', () => { test('should map query filters', async () => { const before = { meta: { index: 'logstash-*' }, query: { match: { _type: { query: 'apache', type: 'phrase' } } }, }; - const after = mapFilter(before as Filter); + const after = mapFilter(before as Filter) as PhraseFilter; expect(after).toHaveProperty('meta'); expect(after.meta).toHaveProperty('key', '_type'); expect(after.meta).toHaveProperty('value'); - expect(getDisplayName(after)).toBe('apache'); + expect(getDisplayValueFromFilter(after, [])).toBe('apache'); expect(after.meta).toHaveProperty('disabled', false); expect(after.meta).toHaveProperty('negate', false); }); @@ -42,7 +37,7 @@ describe('filter manager utilities', () => { expect(after).toHaveProperty('meta'); expect(after.meta).toHaveProperty('key', '@timestamp'); expect(after.meta).toHaveProperty('value'); - expect(getDisplayName(after)).toBe('exists'); + expect(getDisplayValueFromFilter(after, [])).toBe('exists'); expect(after.meta).toHaveProperty('disabled', false); expect(after.meta).toHaveProperty('negate', false); }); @@ -54,7 +49,7 @@ describe('filter manager utilities', () => { expect(after).toHaveProperty('meta'); expect(after.meta).toHaveProperty('key', 'query'); expect(after.meta).toHaveProperty('value'); - expect(getDisplayName(after)).toBe('{"test":{}}'); + expect(getDisplayValueFromFilter(after, [])).toBe('{"test":{}}'); expect(after.meta).toHaveProperty('disabled', false); expect(after.meta).toHaveProperty('negate', false); }); diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrase.test.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrase.test.ts index c63a8f82f1704f..88e819bb7f9a7b 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrase.test.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrase.test.ts @@ -6,12 +6,13 @@ * Side Public License, v 1. */ -import { mapPhrase } from './map_phrase'; +import { getPhraseDisplayValue, mapPhrase } from './map_phrase'; import type { PhraseFilter, Filter } from '@kbn/es-query'; +import { FieldFormat } from '@kbn/field-formats-plugin/common'; describe('filter manager utilities', () => { describe('mapPhrase()', () => { - test('should return the key and value for matching filters', async () => { + test('should return the key for matching filters', async () => { const filter = { meta: { index: 'logstash-*' }, query: { match: { _type: { query: 'apache', type: 'phrase' } } }, @@ -19,13 +20,7 @@ describe('filter manager utilities', () => { const result = mapPhrase(filter); - expect(result).toHaveProperty('value'); expect(result).toHaveProperty('key', '_type'); - - if (result.value) { - const displayName = result.value(); - expect(displayName).toBe('apache'); - } }); test('should return undefined for none matching', async (done) => { @@ -42,4 +37,31 @@ describe('filter manager utilities', () => { } }); }); + + describe('getPhraseDisplayValue()', () => { + test('without formatter with value', () => { + const filter = { meta: { value: 'hello' } } as PhraseFilter; + const result = getPhraseDisplayValue(filter); + expect(result).toMatchInlineSnapshot(`"hello"`); + }); + + test('without formatter empty value', () => { + const filter = { meta: { value: '' } } as PhraseFilter; + const result = getPhraseDisplayValue(filter); + expect(result).toMatchInlineSnapshot(`""`); + }); + + test('without formatter with undefined value', () => { + const filter = { meta: { params: {} } } as PhraseFilter; + const result = getPhraseDisplayValue(filter); + expect(result).toMatchInlineSnapshot(`""`); + }); + + test('with formatter', () => { + const filter = { meta: { value: 'hello' } } as PhraseFilter; + const formatter = { convert: (val) => `formatted ${val}` } as FieldFormat; + const result = getPhraseDisplayValue(filter, formatter); + expect(result).toMatchInlineSnapshot(`"formatted hello"`); + }); + }); }); diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrase.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrase.ts index 23cae0ee852ca8..fe80db37ee36c9 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrase.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrase.ts @@ -6,27 +6,27 @@ * Side Public License, v 1. */ +import type { Filter, PhraseFilter, ScriptedPhraseFilter } from '@kbn/es-query'; import { get } from 'lodash'; import { - PhraseFilter, getPhraseFilterValue, getPhraseFilterField, FILTERS, isScriptedPhraseFilter, - Filter, isPhraseFilter, } from '@kbn/es-query'; - -import { FilterValueFormatter } from '../../../../../common'; +import { FieldFormat } from '@kbn/field-formats-plugin/common'; const getScriptedPhraseValue = (filter: PhraseFilter) => get(filter, ['query', 'script', 'script', 'params', 'value']); -const getFormattedValueFn = (value: any) => { - return (formatter?: FilterValueFormatter) => { - return formatter ? formatter.convert(value) : value; - }; -}; +export function getPhraseDisplayValue( + filter: PhraseFilter | ScriptedPhraseFilter, + formatter?: FieldFormat +) { + const value = filter.meta.value ?? filter.meta.params.query; + return formatter?.convert(value) ?? value ?? ''; +} const getParams = (filter: PhraseFilter) => { const scriptedPhraseValue = getScriptedPhraseValue(filter); @@ -39,7 +39,6 @@ const getParams = (filter: PhraseFilter) => { key, params, type: FILTERS.PHRASE, - value: getFormattedValueFn(query), }; }; diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrases.test.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrases.test.ts new file mode 100644 index 00000000000000..4a219e23aff09c --- /dev/null +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrases.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ + +import type { PhrasesFilter, Filter } from '@kbn/es-query'; +import { FILTERS } from '@kbn/es-query'; +import { getPhrasesDisplayValue, mapPhrases } from './map_phrases'; +import { FieldFormat } from '@kbn/field-formats-plugin/common'; + +describe('filter manager utilities', () => { + describe('mapPhrases()', () => { + test('should return the key and value for matching filters', async () => { + const filter = { + meta: { + type: FILTERS.PHRASES, + index: 'logstash-*', + key: '_type', + params: ['hello', 1, 'world'], + }, + } as PhrasesFilter; + + const result = mapPhrases(filter); + + expect(result).toHaveProperty('key', '_type'); + expect(result).toHaveProperty('value', ['hello', 1, 'world']); + }); + + test('should return undefined for none matching', async (done) => { + const filter = { + meta: { index: 'logstash-*' }, + query: { query_string: { query: 'foo:bar' } }, + } as Filter; + + try { + mapPhrases(filter); + } catch (e) { + expect(e).toBe(filter); + done(); + } + }); + }); + + describe('getPhrasesDisplayValue()', () => { + test('without formatter', () => { + const filter = { meta: { params: ['hello', 1, 'world'] } } as PhrasesFilter; + const result = getPhrasesDisplayValue(filter); + expect(result).toMatchInlineSnapshot(`"hello, 1, world"`); + }); + + test('with formatter', () => { + const filter = { meta: { params: ['hello', 1, 'world'] } } as PhrasesFilter; + const formatter = { convert: (val) => `formatted ${val}` } as FieldFormat; + const result = getPhrasesDisplayValue(filter, formatter); + expect(result).toMatchInlineSnapshot(`"formatted hello, formatted 1, formatted world"`); + }); + }); +}); diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrases.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrases.ts index 9ffdd3070e43a5..48ca3852e715d2 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrases.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrases.ts @@ -6,19 +6,16 @@ * Side Public License, v 1. */ -import { Filter, isPhrasesFilter } from '@kbn/es-query'; +import { Filter, PhrasesFilter, isPhrasesFilter } from '@kbn/es-query'; +import { FieldFormat } from '@kbn/field-formats-plugin/common'; -import { FilterValueFormatter } from '../../../../../common'; - -const getFormattedValueFn = (params: any) => { - return (formatter?: FilterValueFormatter) => { - return params - .map((v: any) => { - return formatter ? formatter.convert(v) : v; - }) - .join(', '); - }; -}; +export function getPhrasesDisplayValue(filter: PhrasesFilter, formatter?: FieldFormat) { + return filter.meta.params + .map((v: string) => { + return formatter?.convert(v) ?? v; + }) + .join(', '); +} export const mapPhrases = (filter: Filter) => { if (!isPhrasesFilter(filter)) { @@ -30,7 +27,7 @@ export const mapPhrases = (filter: Filter) => { return { type, key, - value: getFormattedValueFn(params), + value: params, params, }; }; diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_range.test.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_range.test.ts index 3b82ee6ef0f1cd..065e3da1899990 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_range.test.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_range.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { mapRange } from './map_range'; +import { getRangeDisplayValue, mapRange } from './map_range'; import { FilterMeta, RangeFilter, Filter } from '@kbn/es-query'; describe('filter manager utilities', () => { @@ -19,11 +19,7 @@ describe('filter manager utilities', () => { const result = mapRange(filter); expect(result).toHaveProperty('key', 'bytes'); - expect(result).toHaveProperty('value'); - if (result.value) { - const displayName = result.value(); - expect(displayName).toBe('1024 to 2048'); - } + expect(result).toHaveProperty('value', { gt: 1024, lt: 2048 }); }); test('should return undefined for none matching', async (done) => { @@ -41,4 +37,62 @@ describe('filter manager utilities', () => { } }); }); + + describe('getRangeDisplayValue()', () => { + test('gt & lt', () => { + const params = { gt: 10, lt: 100 }; + const filter = { meta: { params } } as RangeFilter; + const result = getRangeDisplayValue(filter); + expect(result).toMatchInlineSnapshot(`"10 to 100"`); + }); + + test('gt & lte', () => { + const params = { gt: 20, lte: 200 }; + const filter = { meta: { params } } as RangeFilter; + const result = getRangeDisplayValue(filter); + expect(result).toMatchInlineSnapshot(`"20 to 200"`); + }); + + test('gte & lt', () => { + const params = { gte: 'low', lt: 'high' }; + const filter = { meta: { params } } as RangeFilter; + const result = getRangeDisplayValue(filter); + expect(result).toMatchInlineSnapshot(`"low to high"`); + }); + + test('gte & lte', () => { + const params = { gte: 40, lte: 400 }; + const filter = { meta: { params } } as RangeFilter; + const result = getRangeDisplayValue(filter); + expect(result).toMatchInlineSnapshot(`"40 to 400"`); + }); + + test('gt', () => { + const params = { gt: 50 }; + const filter = { meta: { params } } as RangeFilter; + const result = getRangeDisplayValue(filter); + expect(result).toMatchInlineSnapshot(`"50 to Infinity"`); + }); + + test('gte', () => { + const params = { gte: 60 }; + const filter = { meta: { params } } as RangeFilter; + const result = getRangeDisplayValue(filter); + expect(result).toMatchInlineSnapshot(`"60 to Infinity"`); + }); + + test('lt', () => { + const params = { lt: 70 }; + const filter = { meta: { params } } as RangeFilter; + const result = getRangeDisplayValue(filter); + expect(result).toMatchInlineSnapshot(`"-Infinity to 70"`); + }); + + test('lte', () => { + const params = { lte: 80 }; + const filter = { meta: { params } } as RangeFilter; + const result = getRangeDisplayValue(filter); + expect(result).toMatchInlineSnapshot(`"-Infinity to 80"`); + }); + }); }); diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_range.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_range.ts index c5fa5ccc899578..04eb67e7921637 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_range.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_range.ts @@ -6,21 +6,27 @@ * Side Public License, v 1. */ -import { get, hasIn } from 'lodash'; -import { RangeFilter, isScriptedRangeFilter, isRangeFilter, Filter, FILTERS } from '@kbn/es-query'; - -import { FilterValueFormatter } from '../../../../../common'; - -const getFormattedValueFn = (left: any, right: any) => { - return (formatter?: FilterValueFormatter) => { - let displayValue = `${left} to ${right}`; - if (formatter) { - const convert = formatter.getConverterFor('text'); - displayValue = `${convert(left)} to ${convert(right)}`; - } - return displayValue; - }; -}; +import { get } from 'lodash'; +import { + ScriptedRangeFilter, + RangeFilter, + isScriptedRangeFilter, + isRangeFilter, + Filter, + FILTERS, +} from '@kbn/es-query'; +import { FieldFormat } from '@kbn/field-formats-plugin/common'; + +export function getRangeDisplayValue( + { meta: { params } }: RangeFilter | ScriptedRangeFilter, + formatter?: FieldFormat +) { + const left = params.gte ?? params.gt ?? -Infinity; + const right = params.lte ?? params.lt ?? Infinity; + if (!formatter) return `${left} to ${right}`; + const convert = formatter.getConverterFor('text'); + return `${convert(left)} to ${convert(right)}`; +} const getFirstRangeKey = (filter: RangeFilter) => filter.query.range && Object.keys(filter.query.range)[0]; @@ -33,15 +39,7 @@ function getParams(filter: RangeFilter) { ? get(filter.query, 'script.script.params') : getRangeByKey(filter, key); - let left = hasIn(params, 'gte') ? params.gte : params.gt; - if (left == null) left = -Infinity; - - let right = hasIn(params, 'lte') ? params.lte : params.lt; - if (right == null) right = Infinity; - - const value = getFormattedValueFn(left, right); - - return { type: FILTERS.RANGE, key, value, params }; + return { type: FILTERS.RANGE, key, value: params, params }; } export const isMapRangeFilter = (filter: any): filter is RangeFilter => diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index 58b66bb74b5e3c..fe803b76364d88 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -6,14 +6,12 @@ * Side Public License, v 1. */ -import { CoreStart } from '@kbn/core/public'; import { BfetchPublicSetup } from '@kbn/bfetch-plugin/public'; -import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { ExpressionsSetup } from '@kbn/expressions-plugin/public'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public'; import { FieldFormatsSetup, FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import { UsageCollectionSetup, UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; +import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { Setup as InspectorSetup } from '@kbn/inspector-plugin/public'; import { ScreenshotModePluginStart } from '@kbn/screenshot-mode-plugin/public'; import { SharePluginStart } from '@kbn/share-plugin/public'; @@ -102,15 +100,3 @@ export interface DataPublicPluginStart { nowProvider: NowProviderPublicContract; } - -export interface IDataPluginServices extends Partial { - appName: string; - uiSettings: CoreStart['uiSettings']; - savedObjects: CoreStart['savedObjects']; - notifications: CoreStart['notifications']; - application: CoreStart['application']; - http: CoreStart['http']; - storage: IStorageWrapper; - data: DataPublicPluginStart; - usageCollection?: UsageCollectionStart; -} diff --git a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.test.ts b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.test.ts index 32f2d8b1e03d5b..7394f51f861ef6 100644 --- a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.test.ts +++ b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.test.ts @@ -10,6 +10,7 @@ import type { Logger } from '@kbn/core/server'; import { eqlSearchStrategyProvider } from './eql_search_strategy'; import { SearchStrategyDependencies } from '../../types'; import { EqlSearchStrategyRequest } from '../../../../common'; +import { firstValueFrom } from 'rxjs'; const getMockEqlResponse = () => ({ body: { @@ -84,9 +85,15 @@ describe('EQL search strategy', () => { await eqlSearch.search({ options, params }, {}, mockDeps).toPromise(); const [[request, requestOptions]] = mockEqlSearch.mock.calls; - expect(request.index).toEqual('logstash-*'); - expect(request.body).toEqual(expect.objectContaining({ query: 'process where 1 == 1' })); - expect(requestOptions).toEqual(expect.objectContaining({ ignore: [400] })); + expect(request).toEqual({ + body: { query: 'process where 1 == 1' }, + ignore_unavailable: true, + index: 'logstash-*', + keep_alive: '1m', + max_concurrent_shard_requests: undefined, + wait_for_completion_timeout: '100ms', + }); + expect(requestOptions).toEqual({ ignore: [400], meta: true, signal: undefined }); }); it('retrieves the current request if an id is provided', async () => { @@ -95,7 +102,11 @@ describe('EQL search strategy', () => { const [[requestParams]] = mockEqlGet.mock.calls; expect(mockEqlSearch).not.toHaveBeenCalled(); - expect(requestParams).toEqual(expect.objectContaining({ id: 'my-search-id' })); + expect(requestParams).toEqual({ + id: 'my-search-id', + keep_alive: '1m', + wait_for_completion_timeout: '100ms', + }); }); it('emits an error if the client throws', async () => { @@ -184,7 +195,7 @@ describe('EQL search strategy', () => { ); }); - it('passes transport options for an existing request', async () => { + it('passes (deprecated) transport options for an existing request', async () => { const eqlSearch = await eqlSearchStrategyProvider(mockLogger); await eqlSearch .search({ id: 'my-search-id', options: { ignore: [400] } }, {}, mockDeps) @@ -194,6 +205,42 @@ describe('EQL search strategy', () => { expect(mockEqlSearch).not.toHaveBeenCalled(); expect(requestOptions).toEqual(expect.objectContaining({ ignore: [400] })); }); + + it('passes abort signal', async () => { + const eqlSearch = eqlSearchStrategyProvider(mockLogger); + const eql: EqlSearchStrategyRequest = { id: 'my-search-id' }; + const abortController = new AbortController(); + await firstValueFrom( + eqlSearch.search(eql, { abortSignal: abortController.signal }, mockDeps) + ); + const [[_params, requestOptions]] = mockEqlGet.mock.calls; + + expect(requestOptions).toEqual({ meta: true, signal: expect.any(AbortSignal) }); + }); + + it('passes transport options for search with id', async () => { + const eqlSearch = eqlSearchStrategyProvider(mockLogger); + const eql: EqlSearchStrategyRequest = { id: 'my-search-id' }; + await firstValueFrom( + eqlSearch.search(eql, { transport: { maxResponseSize: 13131313 } }, mockDeps) + ); + const [[_params, requestOptions]] = mockEqlGet.mock.calls; + + expect(requestOptions).toEqual({ + maxResponseSize: 13131313, + meta: true, + signal: undefined, + }); + }); + + it('passes transport options for search without id', async () => { + const eqlSearch = eqlSearchStrategyProvider(mockLogger); + const eql: EqlSearchStrategyRequest = { params: { index: 'all' } }; + await firstValueFrom(eqlSearch.search(eql, { transport: { ignore: [400] } }, mockDeps)); + const [[_params, requestOptions]] = mockEqlSearch.mock.calls; + + expect(requestOptions).toEqual({ ignore: [400], meta: true, signal: undefined }); + }); }); describe('response', () => { diff --git a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts index 13b4295fb7c636..aab1341f9dbfa5 100644 --- a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts @@ -56,12 +56,18 @@ export const eqlSearchStrategyProvider = ( const response = id ? await client.get( { ...params, id }, - { ...request.options, signal: options.abortSignal, meta: true } + { + ...request.options, + ...options.transport, + signal: options.abortSignal, + meta: true, + } ) : // @ts-expect-error optional key cannot be used since search doesn't expect undefined await client.search(params as EqlSearchStrategyRequest['params'], { ...request.options, - abortController: { signal: options.abortSignal }, + ...options.transport, + signal: options.abortSignal, meta: true, }); diff --git a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts index 7a7f2253b96950..d8413703808c7a 100644 --- a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts +++ b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts @@ -15,6 +15,7 @@ import { SearchStrategyDependencies } from '../../types'; import * as indexNotFoundException from '../../../../common/search/test_data/index_not_found_exception.json'; import { errors } from '@elastic/elasticsearch'; import { KbnServerError } from '@kbn/kibana-utils-plugin/server'; +import { firstValueFrom } from 'rxjs'; describe('ES search strategy', () => { const successBody = { @@ -105,6 +106,19 @@ describe('ES search strategy', () => { done(); })); + it('calls the client with transport options', async () => { + const params = { index: 'logstash-*', ignore_unavailable: false, timeout: '1000ms' }; + await firstValueFrom( + esSearchStrategyProvider(mockConfig$, mockLogger).search( + { params }, + { transport: { maxRetries: 5 } }, + getMockedDeps() + ) + ); + const [, searchOptions] = esClient.search.mock.calls[0]; + expect(searchOptions).toEqual({ signal: undefined, maxRetries: 5 }); + }); + it('can be aborted', async () => { const params = { index: 'logstash-*', ignore_unavailable: false, timeout: '1000ms' }; @@ -120,6 +134,7 @@ describe('ES search strategy', () => { ...params, track_total_hits: true, }); + expect(esClient.search.mock.calls[0][1]).toEqual({ signal: expect.any(AbortSignal) }); }); it('throws normalized error if ResponseError is thrown', async (done) => { diff --git a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts index 741174ac01613a..73a3b587048771 100644 --- a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts @@ -28,7 +28,7 @@ export const esSearchStrategyProvider = ( * @throws `KbnServerError` * @returns `Observable>` */ - search: (request, { abortSignal, ...options }, { esClient, uiSettingsClient }) => { + search: (request, { abortSignal, transport, ...options }, { esClient, uiSettingsClient }) => { // Only default index pattern type is supported here. // See ese for other type support. if (request.indexType) { @@ -48,6 +48,7 @@ export const esSearchStrategyProvider = ( }; const body = await esClient.asCurrentUser.search(params, { signal: abortSignal, + ...transport, }); const response = shimHitsTotal(body, options); return toKibanaSearchResponse(response); diff --git a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.test.ts b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.test.ts index b657d5602e99c6..409c84a4638f7b 100644 --- a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.test.ts +++ b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { KbnServerError } from '@kbn/kibana-utils-plugin/server'; import { errors } from '@elastic/elasticsearch'; import * as indexNotFoundException from '../../../../common/search/test_data/index_not_found_exception.json'; @@ -119,6 +119,53 @@ describe('ES search strategy', () => { expect(request).toHaveProperty('keep_alive', '1m'); }); + it('sets transport options on POST requests', async () => { + const transportOptions = { maxRetries: 1 }; + mockSubmitCaller.mockResolvedValueOnce(mockAsyncResponse); + const params = { index: 'logstash-*', body: { query: {} } }; + const esSearch = enhancedEsSearchStrategyProvider(mockLegacyConfig$, mockLogger); + + await firstValueFrom( + esSearch.search({ params }, { transport: transportOptions }, mockDeps) + ); + + expect(mockSubmitCaller).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + batched_reduce_size: 64, + body: { query: {} }, + ignore_unavailable: true, + index: 'logstash-*', + keep_alive: '1m', + keep_on_completion: false, + max_concurrent_shard_requests: undefined, + track_total_hits: true, + wait_for_completion_timeout: '100ms', + }), + expect.objectContaining({ maxRetries: 1, meta: true, signal: undefined }) + ); + }); + + it('sets transport options on GET requests', async () => { + mockGetCaller.mockResolvedValueOnce(mockAsyncResponse); + const params = { index: 'logstash-*', body: { query: {} } }; + const esSearch = enhancedEsSearchStrategyProvider(mockLegacyConfig$, mockLogger); + + await firstValueFrom( + esSearch.search({ id: 'foo', params }, { transport: { maxRetries: 1 } }, mockDeps) + ); + + expect(mockGetCaller).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + id: 'foo', + keep_alive: '1m', + wait_for_completion_timeout: '100ms', + }), + expect.objectContaining({ maxRetries: 1, meta: true, signal: undefined }) + ); + }); + it('sets wait_for_completion_timeout and keep_alive in the request', async () => { mockSubmitCaller.mockResolvedValueOnce(mockAsyncResponse); diff --git a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts index 450407800e7af9..33234d7c657305 100644 --- a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts @@ -70,9 +70,10 @@ export const enhancedEsSearchStrategyProvider = ( const { body, headers } = id ? await client.asyncSearch.get( { ...params, id }, - { signal: options.abortSignal, meta: true } + { ...options.transport, signal: options.abortSignal, meta: true } ) : await client.asyncSearch.submit(params, { + ...options.transport, signal: options.abortSignal, meta: true, }); diff --git a/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.test.ts b/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.test.ts index 4b700aa0c69495..e36b66a31c017f 100644 --- a/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.test.ts +++ b/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.test.ts @@ -72,14 +72,25 @@ describe('SQL search strategy', () => { }; const esSearch = await sqlSearchStrategyProvider(mockLogger); - await esSearch.search({ params }, {}, mockDeps).toPromise(); + await esSearch + .search({ params }, { transport: { requestTimeout: 30000 } }, mockDeps) + .toPromise(); expect(mockSqlQuery).toBeCalled(); - const request = mockSqlQuery.mock.calls[0][0]; - expect(request.query).toEqual(params.query); - expect(request).toHaveProperty('format', 'json'); - expect(request).toHaveProperty('keep_alive', '1m'); - expect(request).toHaveProperty('wait_for_completion_timeout'); + const [request, searchOptions] = mockSqlQuery.mock.calls[0]; + expect(request).toEqual({ + format: 'json', + keep_alive: '1m', + keep_on_completion: undefined, + query: + 'SELECT customer_first_name FROM kibana_sample_data_ecommerce ORDER BY order_date DESC', + wait_for_completion_timeout: '100ms', + }); + expect(searchOptions).toEqual({ + meta: true, + requestTimeout: 30000, + signal: undefined, + }); }); it('makes a GET request to async search with ID', async () => { @@ -92,14 +103,23 @@ describe('SQL search strategy', () => { const esSearch = await sqlSearchStrategyProvider(mockLogger); - await esSearch.search({ id: 'foo', params }, {}, mockDeps).toPromise(); + await esSearch + .search({ id: 'foo', params }, { transport: { requestTimeout: 30000 } }, mockDeps) + .toPromise(); expect(mockSqlGetAsync).toBeCalled(); - const request = mockSqlGetAsync.mock.calls[0][0]; - expect(request.id).toEqual('foo'); - expect(request).toHaveProperty('wait_for_completion_timeout'); - expect(request).toHaveProperty('keep_alive', '1m'); - expect(request).toHaveProperty('format', 'json'); + const [request, searchOptions] = mockSqlGetAsync.mock.calls[0]; + expect(request).toEqual({ + format: 'json', + id: 'foo', + keep_alive: '1m', + wait_for_completion_timeout: '100ms', + }); + expect(searchOptions).toEqual({ + meta: true, + requestTimeout: 30000, + signal: undefined, + }); }); }); diff --git a/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.ts b/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.ts index bb6f6dfa2205a6..b84dcfe45d2cf7 100644 --- a/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.ts @@ -59,10 +59,7 @@ export const sqlSearchStrategyProvider = ( ...getDefaultAsyncGetParams(sessionConfig, options), id, }, - { - signal: options.abortSignal, - meta: true, - } + { ...options.transport, signal: options.abortSignal, meta: true } )); } else { ({ headers, body } = await client.sql.query( @@ -71,10 +68,7 @@ export const sqlSearchStrategyProvider = ( ...getDefaultAsyncSubmitParams(sessionConfig, options), ...params, }, - { - signal: options.abortSignal, - meta: true, - } + { ...options.transport, signal: options.abortSignal, meta: true } )); } diff --git a/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor.test.tsx b/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor.test.tsx index 521d7aff8f976b..50dce256792528 100644 --- a/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor.test.tsx +++ b/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor.test.tsx @@ -112,7 +112,7 @@ describe('', () => { expect(lastState.submit).toBeDefined(); const { data: formData } = await submitFormAndGetData(lastState); - expect(formData).toEqual(field); + expect(formData).toEqual({ ...field, format: null }); // Make sure that both isValid and isSubmitted state are now "true" lastState = getLastStateUpdate(); @@ -128,7 +128,10 @@ describe('', () => { onChange, }, { - namesNotAllowed: existingFields, + namesNotAllowed: { + fields: existingFields, + runtimeComposites: [], + }, existingConcreteFields: [], fieldTypeToProcess: 'runtime', } @@ -165,7 +168,10 @@ describe('', () => { onChange, }, { - namesNotAllowed: existingRuntimeFieldNames, + namesNotAllowed: { + fields: existingRuntimeFieldNames, + runtimeComposites: [], + }, existingConcreteFields: [], fieldTypeToProcess: 'runtime', } diff --git a/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts b/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts index 63eca247cca6f8..51cd024f0b53e0 100644 --- a/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts +++ b/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts @@ -88,7 +88,7 @@ describe('', () => { expect(onSave).toHaveBeenCalled(); const fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0]; - expect(fieldReturned).toEqual(field); + expect(fieldReturned).toEqual({ ...field, format: null }); }); test('should accept an onCancel prop', async () => { @@ -149,6 +149,7 @@ describe('', () => { name: 'someName', type: 'keyword', // default to keyword script: { source: 'echo("hello")' }, + format: null, }); // Change the type and make sure it is forwarded @@ -165,6 +166,7 @@ describe('', () => { name: 'someName', type: 'date', script: { source: 'echo("hello")' }, + format: null, }); }); @@ -202,6 +204,7 @@ describe('', () => { name: 'someName', type: 'keyword', script: { source: 'echo("hello")' }, + format: null, }); }); }); diff --git a/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts b/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts index 5dd0045ab5d686..8659e12909763e 100644 --- a/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts +++ b/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts @@ -813,4 +813,48 @@ describe('Field editor Preview panel', () => { expect(exists('previewNotAvailableCallout')).toBe(true); }); }); + + describe('composite runtime field', () => { + test('should display composite editor when composite type is selected', async () => { + testBed = await setup(); + const { + exists, + actions: { fields, waitForUpdates }, + } = testBed; + fields.updateType('composite', 'Composite'); + await waitForUpdates(); + expect(exists('compositeEditor')).toBe(true); + }); + + test('should show composite field types and update appropriately', async () => { + httpRequestsMockHelpers.setFieldPreviewResponse({ values: { 'composite_field.a': [1] } }); + testBed = await setup(); + const { + exists, + actions: { fields, waitForUpdates }, + } = testBed; + await fields.updateType('composite', 'Composite'); + await fields.updateScript("emit('a',1)"); + await waitForUpdates(); + expect(exists('typeField_0')).toBe(true); + + // increase the number of fields + httpRequestsMockHelpers.setFieldPreviewResponse({ + values: { 'composite_field.a': [1], 'composite_field.b': [1] }, + }); + await fields.updateScript("emit('a',1); emit('b',1)"); + await waitForUpdates(); + expect(exists('typeField_0')).toBe(true); + expect(exists('typeField_1')).toBe(true); + + // decrease the number of fields + httpRequestsMockHelpers.setFieldPreviewResponse({ + values: { 'composite_field.a': [1] }, + }); + await fields.updateScript("emit('a',1)"); + await waitForUpdates(); + expect(exists('typeField_0')).toBe(true); + expect(exists('typeField_1')).toBe(false); + }); + }); }); diff --git a/src/plugins/data_view_field_editor/__jest__/client_integration/helpers/setup_environment.tsx b/src/plugins/data_view_field_editor/__jest__/client_integration/helpers/setup_environment.tsx index 9979e96261e7be..4f7cc3e57a9753 100644 --- a/src/plugins/data_view_field_editor/__jest__/client_integration/helpers/setup_environment.tsx +++ b/src/plugins/data_view_field_editor/__jest__/client_integration/helpers/setup_environment.tsx @@ -12,7 +12,7 @@ import './jest.mocks'; import React, { FunctionComponent } from 'react'; import { merge } from 'lodash'; -import { defer } from 'rxjs'; +import { defer, BehaviorSubject } from 'rxjs'; import { notificationServiceMock, uiSettingsServiceMock } from '@kbn/core/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { fieldFormatsMock as fieldFormats } from '@kbn/field-formats-plugin/common/mocks'; @@ -21,6 +21,7 @@ import { FieldEditorProvider, Context } from '../../../public/components/field_e import { FieldPreviewProvider } from '../../../public/components/preview'; import { initApi, ApiService } from '../../../public/lib'; import { init as initHttpRequests } from './http_requests'; +import { RuntimeFieldSubFields } from '../../../public/shared_imports'; const dataStart = dataPluginMock.createStartContract(); const { search } = dataStart; @@ -124,7 +125,7 @@ export const WithFieldEditorDependencies = uiSettings: uiSettingsServiceMock.createStartContract(), fieldTypeToProcess: 'runtime', existingConcreteFields: [], - namesNotAllowed: [], + namesNotAllowed: { fields: [], runtimeComposites: [] }, links: { runtimePainless: 'https://elastic.co', }, @@ -138,6 +139,8 @@ export const WithFieldEditorDependencies = getById: () => undefined, }, fieldFormats, + fieldName$: new BehaviorSubject(''), + subfields$: new BehaviorSubject(undefined), }; const mergedDependencies = merge({}, dependencies, overridingDependencies); diff --git a/src/plugins/data_view_field_editor/public/components/delete_field_provider.tsx b/src/plugins/data_view_field_editor/public/components/delete_field_provider.tsx index 9a960674e061e8..fc09b860f705ae 100644 --- a/src/plugins/data_view_field_editor/public/components/delete_field_provider.tsx +++ b/src/plugins/data_view_field_editor/public/components/delete_field_provider.tsx @@ -15,7 +15,14 @@ import { CloseEditor } from '../types'; type DeleteFieldFunc = (fieldName: string | string[]) => void; export interface Props { children: (deleteFieldHandler: DeleteFieldFunc) => React.ReactNode; + /** + * Data view of fields to be deleted + */ dataView: DataView; + /** + * Callback fired when fields are deleted + * @param fieldNames - the names of the deleted fields + */ onDelete?: (fieldNames: string[]) => void; } diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/composite_editor.tsx b/src/plugins/data_view_field_editor/public/components/field_editor/composite_editor.tsx new file mode 100644 index 00000000000000..49761aa1228440 --- /dev/null +++ b/src/plugins/data_view_field_editor/public/components/field_editor/composite_editor.tsx @@ -0,0 +1,120 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { + EuiNotificationBadge, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, + EuiFieldText, + EuiComboBox, + EuiFormRow, + EuiButtonEmpty, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import useObservable from 'react-use/lib/useObservable'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { ScriptField } from './form_fields'; +import { useFieldEditorContext } from '../field_editor_context'; +import { RUNTIME_FIELD_OPTIONS_PRIMITIVE } from './constants'; +import { valueToComboBoxOption } from './lib'; +import { RuntimePrimitiveTypes } from '../../shared_imports'; + +export interface CompositeEditorProps { + onReset: () => void; +} + +export const CompositeEditor = ({ onReset }: CompositeEditorProps) => { + const { links, existingConcreteFields, subfields$ } = useFieldEditorContext(); + const subfields = useObservable(subfields$) || {}; + + return ( +
+ + + <> + + + + + + + + + + {Object.entries(subfields).length} + + + + + + + + + + {Object.entries(subfields).map(([key, itemValue], idx) => { + return ( +
+ + + + + + + { + if (newValue.length === 0) { + // Don't allow clearing the type. One must always be selected + return; + } + // update the type for the given field + subfields[key] = { type: newValue[0].value! as RuntimePrimitiveTypes }; + + subfields$.next({ ...subfields }); + }} + isClearable={false} + data-test-subj={`typeField_${idx}`} + aria-label={i18n.translate( + 'indexPatternFieldEditor.editor.form.typeSelectAriaLabel', + { + defaultMessage: 'Type select', + } + )} + fullWidth + /> + + + +
+ ); + })} + +
+ ); +}; diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/constants.ts b/src/plugins/data_view_field_editor/public/components/field_editor/constants.ts index e262d3ecbfe459..b8bf2673ac3bd9 100644 --- a/src/plugins/data_view_field_editor/public/components/field_editor/constants.ts +++ b/src/plugins/data_view_field_editor/public/components/field_editor/constants.ts @@ -9,7 +9,7 @@ import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { RuntimeType } from '../../shared_imports'; -export const RUNTIME_FIELD_OPTIONS: Array> = [ +export const RUNTIME_FIELD_OPTIONS_PRIMITIVE: Array> = [ { label: 'Keyword', value: 'keyword', @@ -39,3 +39,11 @@ export const RUNTIME_FIELD_OPTIONS: Array> value: 'geo_point', }, ]; + +export const RUNTIME_FIELD_OPTIONS = [ + ...RUNTIME_FIELD_OPTIONS_PRIMITIVE, + { + label: 'Composite', + value: 'composite', + } as EuiComboBoxOptionOption, +]; diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/field_detail.tsx b/src/plugins/data_view_field_editor/public/components/field_editor/field_detail.tsx new file mode 100644 index 00000000000000..b9db87b65e3cdc --- /dev/null +++ b/src/plugins/data_view_field_editor/public/components/field_editor/field_detail.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiCode } from '@elastic/eui'; +import { AdvancedParametersSection } from './advanced_parameters_section'; +import { FormRow } from './form_row'; +import { PopularityField, FormatField, ScriptField, CustomLabelField } from './form_fields'; +import { useFieldEditorContext } from '../field_editor_context'; + +const geti18nTexts = (): { + [key: string]: { title: string; description: JSX.Element | string }; +} => ({ + customLabel: { + title: i18n.translate('indexPatternFieldEditor.editor.form.customLabelTitle', { + defaultMessage: 'Set custom label', + }), + description: i18n.translate('indexPatternFieldEditor.editor.form.customLabelDescription', { + defaultMessage: `Create a label to display in place of the field name in Discover, Maps, and Visualize. Useful for shortening a long field name. Queries and filters use the original field name.`, + }), + }, + value: { + title: i18n.translate('indexPatternFieldEditor.editor.form.valueTitle', { + defaultMessage: 'Set value', + }), + description: ( + {'_source'}, + }} + /> + ), + }, + + format: { + title: i18n.translate('indexPatternFieldEditor.editor.form.formatTitle', { + defaultMessage: 'Set format', + }), + description: i18n.translate('indexPatternFieldEditor.editor.form.formatDescription', { + defaultMessage: `Set your preferred format for displaying the value. Changing the format can affect the value and prevent highlighting in Discover.`, + }), + }, + + popularity: { + title: i18n.translate('indexPatternFieldEditor.editor.form.popularityTitle', { + defaultMessage: 'Set popularity', + }), + description: i18n.translate('indexPatternFieldEditor.editor.form.popularityDescription', { + defaultMessage: `Adjust the popularity to make the field appear higher or lower in the fields list. By default, Discover orders fields from most selected to least selected.`, + }), + }, +}); + +export const FieldDetail = ({}) => { + const { links, existingConcreteFields, fieldTypeToProcess } = useFieldEditorContext(); + const i18nTexts = geti18nTexts(); + return ( + <> + {/* Set custom label */} + + + + + {/* Set value */} + {fieldTypeToProcess === 'runtime' && ( + + + + )} + + {/* Set custom format */} + + + + + {/* Advanced settings */} + + + + + + + ); +}; diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/field_editor.tsx b/src/plugins/data_view_field_editor/public/components/field_editor/field_editor.tsx index 47b871196be038..88c91ab6457767 100644 --- a/src/plugins/data_view_field_editor/public/components/field_editor/field_editor.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_editor/field_editor.tsx @@ -6,18 +6,10 @@ * Side Public License, v 1. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; import { get } from 'lodash'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiComboBoxOptionOption, - EuiCode, - EuiCallOut, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiCallOut } from '@elastic/eui'; import { Form, @@ -28,6 +20,7 @@ import { UseField, TextField, RuntimeType, + RuntimePrimitiveTypes, } from '../../shared_imports'; import { Field } from '../../types'; import { useFieldEditorContext } from '../field_editor_context'; @@ -35,16 +28,12 @@ import { useFieldPreviewContext } from '../preview'; import { RUNTIME_FIELD_OPTIONS } from './constants'; import { schema } from './form_schema'; -import { getNameFieldConfig } from './lib'; -import { - TypeField, - CustomLabelField, - ScriptField, - FormatField, - PopularityField, -} from './form_fields'; -import { FormRow } from './form_row'; -import { AdvancedParametersSection } from './advanced_parameters_section'; +import { getNameFieldConfig, getFieldPreviewChanges } from './lib'; +import { TypeField } from './form_fields'; +import { FieldDetail } from './field_detail'; +import { CompositeEditor } from './composite_editor'; +import { TypeSelection } from './types'; +import { ChangeType } from '../preview/types'; export interface FieldEditorFormState { isValid: boolean | undefined; @@ -53,8 +42,9 @@ export interface FieldEditorFormState { submit: FormHook['submit']; } -export interface FieldFormInternal extends Omit { - type: Array>; +export interface FieldFormInternal extends Omit { + fields?: Record; + type: TypeSelection; __meta__: { isCustomLabelVisible: boolean; isValueVisible: boolean; @@ -72,66 +62,28 @@ export interface Props { onFormModifiedChange?: (isModified: boolean) => void; } -const geti18nTexts = (): { - [key: string]: { title: string; description: JSX.Element | string }; -} => ({ - customLabel: { - title: i18n.translate('indexPatternFieldEditor.editor.form.customLabelTitle', { - defaultMessage: 'Set custom label', - }), - description: i18n.translate('indexPatternFieldEditor.editor.form.customLabelDescription', { - defaultMessage: `Create a label to display in place of the field name in Discover, Maps, and Visualize. Useful for shortening a long field name. Queries and filters use the original field name.`, - }), - }, - value: { - title: i18n.translate('indexPatternFieldEditor.editor.form.valueTitle', { - defaultMessage: 'Set value', - }), - description: ( - {'_source'}, - }} - /> - ), - }, - format: { - title: i18n.translate('indexPatternFieldEditor.editor.form.formatTitle', { - defaultMessage: 'Set format', - }), - description: i18n.translate('indexPatternFieldEditor.editor.form.formatDescription', { - defaultMessage: `Set your preferred format for displaying the value. Changing the format can affect the value and prevent highlighting in Discover.`, - }), - }, - popularity: { - title: i18n.translate('indexPatternFieldEditor.editor.form.popularityTitle', { - defaultMessage: 'Set popularity', - }), - description: i18n.translate('indexPatternFieldEditor.editor.form.popularityDescription', { - defaultMessage: `Adjust the popularity to make the field appear higher or lower in the fields list. By default, Discover orders fields from most selected to least selected.`, - }), - }, -}); - const changeWarning = i18n.translate('indexPatternFieldEditor.editor.form.changeWarning', { defaultMessage: 'Changing name or type can break searches and visualizations that rely on this field.', }); -const formDeserializer = (field: Field): FieldFormInternal => { - let fieldType: Array>; - if (!field.type) { - fieldType = [RUNTIME_FIELD_OPTIONS[0]]; - } else { - const label = RUNTIME_FIELD_OPTIONS.find(({ value }) => value === field.type)?.label; - fieldType = [{ label: label ?? field.type, value: field.type as RuntimeType }]; +const fieldTypeToComboBoxOption = (type: Field['type']): TypeSelection => { + if (type) { + const label = RUNTIME_FIELD_OPTIONS.find(({ value }) => value === type)?.label; + return [{ label: label ?? type, value: type as RuntimeType }]; } + return [RUNTIME_FIELD_OPTIONS[0]]; +}; + +const formDeserializer = (field: Field): FieldFormInternal => { + const fieldType = fieldTypeToComboBoxOption(field.type); + + const format = field.format === null ? undefined : field.format; return { ...field, type: fieldType, + format, __meta__: { isCustomLabelVisible: field.customLabel !== undefined, isValueVisible: field.script !== undefined, @@ -142,18 +94,21 @@ const formDeserializer = (field: Field): FieldFormInternal => { }; const formSerializer = (field: FieldFormInternal): Field => { - const { __meta__, type, ...rest } = field; + const { __meta__, type, format, ...rest } = field; return { - type: type[0].value!, + type: type && type[0].value!, + // By passing "null" we are explicitly telling DataView to remove the + // format if there is one defined for the field. + format: format === undefined ? null : format, ...rest, }; }; const FieldEditorComponent = ({ field, onChange, onFormModifiedChange }: Props) => { - const { links, namesNotAllowed, existingConcreteFields, fieldTypeToProcess } = - useFieldEditorContext(); + const { namesNotAllowed, fieldTypeToProcess, fieldName$, subfields$ } = useFieldEditorContext(); const { params: { update: updatePreviewParams }, + fieldPreview$, } = useFieldPreviewContext(); const { form } = useForm({ defaultValue: field, @@ -161,10 +116,10 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange }: Props) deserializer: formDeserializer, serializer: formSerializer, }); + const { submit, isValid: isFormValid, isSubmitted, getFields, isSubmitting } = form; const nameFieldConfig = getNameFieldConfig(namesNotAllowed, field); - const i18nTexts = geti18nTexts(); const [formData] = useFormData({ form }); const isFormModified = useFormIsModified({ @@ -177,6 +132,19 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange }: Props) ], }); + // use observable to sidestep react state + useEffect(() => { + const sub = form.subscribe(({ data }) => { + if (data.internal.name !== fieldName$.getValue()) { + fieldName$.next(data.internal.name); + } + }); + + return () => { + sub.unsubscribe(); + }; + }, [form, fieldName$]); + const { name: updatedName, type: updatedType, @@ -189,6 +157,50 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange }: Props) const isValueVisible = get(formData, '__meta__.isValueVisible'); + const resetTypes = useCallback(() => { + const lastVal = fieldPreview$.getValue(); + // resets the preview history to an empty set + fieldPreview$.next([]); + // apply the last preview to get all the types + fieldPreview$.next(lastVal); + }, [fieldPreview$]); + + useEffect(() => { + const existingCompositeField = !!Object.keys(subfields$.getValue() || {}).length; + + const changes$ = getFieldPreviewChanges(fieldPreview$); + + const subChanges = changes$.subscribe((previewFields) => { + const fields = subfields$.getValue(); + + const modifiedFields = { ...fields }; + + Object.entries(previewFields).forEach(([name, change]) => { + if (change.changeType === ChangeType.DELETE) { + delete modifiedFields[name]; + } + if (change.changeType === ChangeType.UPSERT) { + modifiedFields[name] = { type: change.type! }; + } + }); + + subfields$.next(modifiedFields); + // necessary to maintain script code when changing types + form.updateFieldValues({ ...form.getFormData() }); + }); + + // first preview value is skipped for saved fields, need to populate for new fields and rerenders + if (!existingCompositeField) { + fieldPreview$.next([]); + } else if (fieldPreview$.getValue()) { + fieldPreview$.next(fieldPreview$.getValue()); + } + + return () => { + subChanges.unsubscribe(); + }; + }, [form, fieldPreview$, subfields$]); + useEffect(() => { if (onChange) { onChange({ isValid: isFormValid, isSubmitted, isSubmitting, submit }); @@ -202,16 +214,25 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange }: Props) script: isValueVisible === false || Boolean(updatedScript?.source.trim()) === false ? null - : updatedScript, + : { source: updatedScript!.source }, format: updatedFormat?.id !== undefined ? updatedFormat : null, + parentName: field?.parentName, }); - }, [updatedName, updatedType, updatedScript, isValueVisible, updatedFormat, updatePreviewParams]); + }, [ + updatedName, + updatedType, + updatedScript, + isValueVisible, + updatedFormat, + updatePreviewParams, + field, + ]); useEffect(() => { if (onFormModifiedChange) { onFormModifiedChange(isFormModified); } - }, [isFormModified, onFormModifiedChange]); + }, [isFormModified, onFormModifiedChange, form]); return (
- + - {(nameHasChanged || typeHasChanged) && ( <> @@ -259,56 +283,25 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange }: Props) )} - - {/* Set custom label */} - - - - - {/* Set value */} - {fieldTypeToProcess === 'runtime' && ( - - - + {field?.parentName && ( + <> + + + + )} + {updatedType && updatedType[0].value !== 'composite' ? ( + + ) : ( + )} - - {/* Set custom format */} - - - - - {/* Advanced settings */} - - - - - ); }; -export const FieldEditor = React.memo(FieldEditorComponent) as typeof FieldEditorComponent; +export const FieldEditor = FieldEditorComponent as typeof FieldEditorComponent; diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/format_field.tsx b/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/format_field.tsx index d90d7ef6cdf687..9518ba6cc89ed2 100644 --- a/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/format_field.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/format_field.tsx @@ -7,9 +7,14 @@ */ import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; import React, { useEffect, useRef, useState } from 'react'; -import { ES_FIELD_TYPES, UseField, useFormContext, useFormData } from '../../../shared_imports'; +import { + UseField, + useFormData, + ES_FIELD_TYPES, + useFormContext, + SerializedFieldFormat, +} from '../../../shared_imports'; import { useFieldEditorContext } from '../../field_editor_context'; import { FormatSelectEditor } from '../../field_format_editor'; import type { FieldFormInternal } from '../field_editor'; diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/script_field.tsx b/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/script_field.tsx index a7cd508401c6cc..dd66369a37d3f9 100644 --- a/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/script_field.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/script_field.tsx @@ -31,6 +31,7 @@ import type { FieldFormInternal } from '../field_editor'; interface Props { links: { runtimePainless: string }; existingConcreteFields?: Array<{ name: string; type: string }>; + placeholder?: string; } const mapReturnTypeToPainlessContext = (runtimeType: RuntimeType): PainlessContext => { @@ -52,7 +53,7 @@ const mapReturnTypeToPainlessContext = (runtimeType: RuntimeType): PainlessConte } }; -const ScriptFieldComponent = ({ existingConcreteFields, links }: Props) => { +const ScriptFieldComponent = ({ existingConcreteFields, links, placeholder }: Props) => { const monacoEditor = useRef(null); const editorValidationSubscription = useRef(); const fieldCurrentValue = useRef(''); @@ -221,6 +222,7 @@ const ScriptFieldComponent = ({ existingConcreteFields, links }: Props) => { id="runtimeFieldScript" error={errorMessage} isInvalid={!isValid} + data-test-subj="scriptFieldRow" helpText={ { defaultMessage: 'Script editor', } )} + placeholder={placeholder} /> diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/type_field.tsx b/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/type_field.tsx index 36428579a30e86..d4eb463e670518 100644 --- a/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/type_field.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/type_field.tsx @@ -9,18 +9,27 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import { EuiFormRow, EuiComboBox } from '@elastic/eui'; -import { UseField, RuntimeType } from '../../../shared_imports'; -import { RUNTIME_FIELD_OPTIONS } from '../constants'; +import { UseField } from '../../../shared_imports'; +import { RUNTIME_FIELD_OPTIONS, RUNTIME_FIELD_OPTIONS_PRIMITIVE } from '../constants'; +import { TypeSelection } from '../types'; interface Props { isDisabled?: boolean; + includeComposite?: boolean; + path: string; + defaultValue?: TypeSelection; } -export const TypeField = ({ isDisabled = false }: Props) => { +export const TypeField = ({ + isDisabled = false, + includeComposite, + path, + defaultValue = [RUNTIME_FIELD_OPTIONS_PRIMITIVE[0]], +}: Props) => { return ( - >> path="type"> + path={path}> {({ label, value, setValue }) => { if (value === undefined) { return null; @@ -36,8 +45,8 @@ export const TypeField = ({ isDisabled = false }: Props) => { } )} singleSelection={{ asPlainText: true }} - options={RUNTIME_FIELD_OPTIONS} - selectedOptions={value} + options={includeComposite ? RUNTIME_FIELD_OPTIONS : RUNTIME_FIELD_OPTIONS_PRIMITIVE} + selectedOptions={value || defaultValue} onChange={(newValue) => { if (newValue.length === 0) { // Don't allow clearing the type. One must always be selected diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/form_schema.ts b/src/plugins/data_view_field_editor/public/components/field_editor/form_schema.ts index 8d49702b481544..391f54581f2588 100644 --- a/src/plugins/data_view_field_editor/public/components/field_editor/form_schema.ts +++ b/src/plugins/data_view_field_editor/public/components/field_editor/form_schema.ts @@ -139,6 +139,9 @@ export const schema = { }, ], }, + fields: { + defaultValue: {}, + }, __meta__: { isCustomLabelVisible: { defaultValue: false, diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/lib.test.ts b/src/plugins/data_view_field_editor/public/components/field_editor/lib.test.ts new file mode 100644 index 00000000000000..d8a836ea583a30 --- /dev/null +++ b/src/plugins/data_view_field_editor/public/components/field_editor/lib.test.ts @@ -0,0 +1,67 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { getFieldPreviewChanges } from './lib'; +import { BehaviorSubject } from 'rxjs'; +import { ChangeType, FieldPreview } from '../preview/types'; + +describe('getFieldPreviewChanges', () => { + it('should return new keys', (done) => { + const subj = new BehaviorSubject(undefined); + const changes = getFieldPreviewChanges(subj); + changes.subscribe((change) => { + expect(change).toStrictEqual({ hello: { changeType: ChangeType.UPSERT, type: 'keyword' } }); + done(); + }); + subj.next([]); + subj.next([{ key: 'hello', value: 'world', type: 'keyword' }]); + }); + + it('should return updated type', (done) => { + const subj = new BehaviorSubject(undefined); + const changes = getFieldPreviewChanges(subj); + changes.subscribe((change) => { + expect(change).toStrictEqual({ hello: { changeType: ChangeType.UPSERT, type: 'long' } }); + done(); + }); + subj.next([{ key: 'hello', value: 'world', type: 'keyword' }]); + subj.next([{ key: 'hello', value: 1, type: 'long' }]); + }); + + it('should remove keys', (done) => { + const subj = new BehaviorSubject(undefined); + const changes = getFieldPreviewChanges(subj); + changes.subscribe((change) => { + expect(change).toStrictEqual({ hello: { changeType: ChangeType.DELETE } }); + done(); + }); + subj.next([{ key: 'hello', value: 'world', type: 'keyword' }]); + subj.next([]); + }); + + it('should add, update, and remove keys in a single change', (done) => { + const subj = new BehaviorSubject(undefined); + const changes = getFieldPreviewChanges(subj); + changes.subscribe((change) => { + expect(change).toStrictEqual({ + hello: { changeType: ChangeType.UPSERT, type: 'long' }, + hello2: { changeType: ChangeType.DELETE }, + hello3: { changeType: ChangeType.UPSERT, type: 'keyword' }, + }); + done(); + }); + subj.next([ + { key: 'hello', value: 'world', type: 'keyword' }, + { key: 'hello2', value: 'world', type: 'keyword' }, + ]); + subj.next([ + { key: 'hello', value: 1, type: 'long' }, + { key: 'hello3', value: 'world', type: 'keyword' }, + ]); + }); +}); diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/lib.ts b/src/plugins/data_view_field_editor/public/components/field_editor/lib.ts index 5b2e66c66fe390..bad85541007909 100644 --- a/src/plugins/data_view_field_editor/public/components/field_editor/lib.ts +++ b/src/plugins/data_view_field_editor/public/components/field_editor/lib.ts @@ -7,16 +7,30 @@ */ import { i18n } from '@kbn/i18n'; +import { map, bufferCount, filter, BehaviorSubject } from 'rxjs'; +import { differenceWith, isEqual } from 'lodash'; import { ValidationFunc, FieldConfig } from '../../shared_imports'; -import { Field } from '../../types'; +import type { Field } from '../../types'; +import type { Context } from '../field_editor_context'; import { schema } from './form_schema'; import type { Props } from './field_editor'; +import { RUNTIME_FIELD_OPTIONS_PRIMITIVE } from './constants'; +import { ChangeType, FieldPreview } from '../preview/types'; + +import { RuntimePrimitiveTypes } from '../../shared_imports'; + +export interface Change { + changeType: ChangeType; + type?: RuntimePrimitiveTypes; +} + +export type ChangeSet = Record; const createNameNotAllowedValidator = - (namesNotAllowed: string[]): ValidationFunc<{}, string, string> => + (namesNotAllowed: Context['namesNotAllowed']): ValidationFunc<{}, string, string> => ({ value }) => { - if (namesNotAllowed.includes(value)) { + if (namesNotAllowed.fields.includes(value)) { return { message: i18n.translate( 'indexPatternFieldEditor.editor.runtimeFieldsEditor.existRuntimeFieldNamesValidationErrorMessage', @@ -25,6 +39,15 @@ const createNameNotAllowedValidator = } ), }; + } else if (namesNotAllowed.runtimeComposites.includes(value)) { + return { + message: i18n.translate( + 'indexPatternFieldEditor.editor.runtimeFieldsEditor.existCompositeNamesValidationErrorMessage', + { + defaultMessage: 'A runtime composite with this name already exists.', + } + ), + }; } }; @@ -36,7 +59,7 @@ const createNameNotAllowedValidator = * @param field Initial value of the form */ export const getNameFieldConfig = ( - namesNotAllowed?: string[], + namesNotAllowed?: Context['namesNotAllowed'], field?: Props['field'] ): FieldConfig => { const nameFieldConfig = schema.name as FieldConfig; @@ -45,16 +68,53 @@ export const getNameFieldConfig = ( return nameFieldConfig; } + const filterOutCurrentFieldName = (name: string) => name !== field?.name; + // Add validation to not allow duplicates return { ...nameFieldConfig!, validations: [ ...(nameFieldConfig.validations ?? []), { - validator: createNameNotAllowedValidator( - namesNotAllowed.filter((name) => name !== field?.name) - ), + validator: createNameNotAllowedValidator({ + fields: namesNotAllowed.fields.filter(filterOutCurrentFieldName), + runtimeComposites: namesNotAllowed.runtimeComposites.filter(filterOutCurrentFieldName), + }), }, ], }; }; + +export const valueToComboBoxOption = (value: string) => + RUNTIME_FIELD_OPTIONS_PRIMITIVE.find(({ value: optionValue }) => optionValue === value); + +export const getFieldPreviewChanges = (subject: BehaviorSubject) => + subject.pipe( + filter((preview) => preview !== undefined), + map((items) => + // reduce the fields to make diffing easier + items!.map((item) => { + const key = item.key.slice(item.key.search('\\.') + 1); + return { name: key, type: item.type! }; + }) + ), + bufferCount(2, 1), + // convert values into diff descriptions + map(([prev, next]) => { + const changes = differenceWith(next, prev, isEqual).reduce((col, item) => { + col[item.name] = { + changeType: ChangeType.UPSERT, + type: item.type as RuntimePrimitiveTypes, + }; + return col; + }, {} as ChangeSet); + + prev.forEach((prevItem) => { + if (!next.find((nextItem) => nextItem.name === prevItem.name)) { + changes[prevItem.name] = { changeType: ChangeType.DELETE }; + } + }); + return changes; + }), + filter((fields) => Object.keys(fields).length > 0) + ); diff --git a/test/visual_regression/ftr_provider_context.ts b/src/plugins/data_view_field_editor/public/components/field_editor/types.ts similarity index 51% rename from test/visual_regression/ftr_provider_context.ts rename to src/plugins/data_view_field_editor/public/components/field_editor/types.ts index 28bedd1ca6bc34..3c8aba9149ea14 100644 --- a/test/visual_regression/ftr_provider_context.ts +++ b/src/plugins/data_view_field_editor/public/components/field_editor/types.ts @@ -6,10 +6,13 @@ * Side Public License, v 1. */ -import { GenericFtrProviderContext, GenericFtrService } from '@kbn/test'; +import { EuiComboBoxOptionOption } from '@elastic/eui'; -import { pageObjects } from '../functional/page_objects'; -import { services } from './services'; +import { RuntimeType } from '../../shared_imports'; -export type FtrProviderContext = GenericFtrProviderContext; -export class FtrService extends GenericFtrService {} +export type TypeSelection = Array>; + +export interface FieldTypeInfo { + name: string; + type: string; +} diff --git a/src/plugins/data_view_field_editor/public/components/field_editor_context.tsx b/src/plugins/data_view_field_editor/public/components/field_editor_context.tsx index 6cadc094bb35ff..494d6034f66690 100644 --- a/src/plugins/data_view_field_editor/public/components/field_editor_context.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_editor_context.tsx @@ -8,8 +8,13 @@ import React, { createContext, useContext, FunctionComponent, useMemo } from 'react'; import { NotificationsStart, CoreStart } from '@kbn/core/public'; -import { FieldFormatsStart } from '../shared_imports'; -import type { DataView, DataPublicPluginStart } from '../shared_imports'; +import type { BehaviorSubject } from 'rxjs'; +import type { + DataView, + DataPublicPluginStart, + FieldFormatsStart, + RuntimeFieldSubFields, +} from '../shared_imports'; import { ApiService } from '../lib/api'; import type { InternalFieldType, PluginStart } from '../types'; @@ -32,7 +37,10 @@ export interface Context { * e.g we probably don't want a user to give a name of an existing * runtime field (for that the user should edit the existing runtime field). */ - namesNotAllowed: string[]; + namesNotAllowed: { + fields: string[]; + runtimeComposites: string[]; + }; /** * An array of existing concrete fields. If the user gives a name to the runtime * field that matches one of the concrete fields, a callout will be displayed @@ -40,6 +48,8 @@ export interface Context { * It is also used to provide the list of field autocomplete suggestions to the code editor. */ existingConcreteFields: Array<{ name: string; type: string }>; + fieldName$: BehaviorSubject; + subfields$: BehaviorSubject; } const fieldEditorContext = createContext(undefined); @@ -55,6 +65,8 @@ export const FieldEditorProvider: FunctionComponent = ({ namesNotAllowed, existingConcreteFields, children, + fieldName$, + subfields$, }) => { const ctx = useMemo( () => ({ @@ -67,6 +79,8 @@ export const FieldEditorProvider: FunctionComponent = ({ fieldFormatEditors, namesNotAllowed, existingConcreteFields, + fieldName$, + subfields$, }), [ dataView, @@ -78,6 +92,8 @@ export const FieldEditorProvider: FunctionComponent = ({ fieldFormatEditors, namesNotAllowed, existingConcreteFields, + fieldName$, + subfields$, ] ); diff --git a/src/plugins/data_view_field_editor/public/components/field_editor_flyout_content.tsx b/src/plugins/data_view_field_editor/public/components/field_editor_flyout_content.tsx index 9968838919513a..edd0a7bc70b627 100644 --- a/src/plugins/data_view_field_editor/public/components/field_editor_flyout_content.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_editor_flyout_content.tsx @@ -20,6 +20,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { euiFlyoutClassname } from '../constants'; import type { Field } from '../types'; import { ModifiedFieldModal, SaveFieldTypeOrNameChangedModal } from './confirm_modals'; + import { FieldEditor, FieldEditorFormState } from './field_editor/field_editor'; import { useFieldEditorContext } from './field_editor_context'; import { FlyoutPanels } from './flyout_panels'; @@ -69,7 +70,8 @@ const FieldEditorFlyoutContentComponent = ({ }: Props) => { const isMounted = useRef(false); const isEditingExistingField = !!fieldToEdit; - const { dataView } = useFieldEditorContext(); + const { dataView, subfields$ } = useFieldEditorContext(); + const { panel: { isVisible: isPanelVisible }, } = useFieldPreviewContext(); @@ -100,7 +102,7 @@ const FieldEditorFlyoutContentComponent = ({ }, [isFormModified]); const onClickSave = useCallback(async () => { - const { isValid, data } = await submit(); + const { isValid, data: updatedField } = await submit(); if (!isMounted.current) { // User has closed the flyout meanwhile submitting the form @@ -108,8 +110,8 @@ const FieldEditorFlyoutContentComponent = ({ } if (isValid) { - const nameChange = fieldToEdit?.name !== data.name; - const typeChange = fieldToEdit?.type !== data.type; + const nameChange = fieldToEdit?.name !== updatedField.name; + const typeChange = fieldToEdit?.type !== updatedField.type; if (isEditingExistingField && (nameChange || typeChange)) { setModalVisibility({ @@ -117,10 +119,14 @@ const FieldEditorFlyoutContentComponent = ({ confirmChangeNameOrType: true, }); } else { - onSave(data); + if (updatedField.type === 'composite') { + onSave({ ...updatedField, fields: subfields$.getValue() }); + } else { + onSave(updatedField); + } } } - }, [onSave, submit, fieldToEdit, isEditingExistingField]); + }, [onSave, submit, fieldToEdit, isEditingExistingField, subfields$]); const onClickCancel = useCallback(() => { const canClose = canCloseValidator(); @@ -136,8 +142,12 @@ const FieldEditorFlyoutContentComponent = ({ { - const { data } = await submit(); - onSave(data); + const { data: updatedField } = await submit(); + if (updatedField.type === 'composite') { + onSave({ ...updatedField, fields: subfields$.getValue() }); + } else { + onSave(updatedField); + } }} onCancel={() => { setModalVisibility(defaultModalVisibility); diff --git a/src/plugins/data_view_field_editor/public/components/field_editor_flyout_content_container.tsx b/src/plugins/data_view_field_editor/public/components/field_editor_flyout_content_container.tsx index 6696cf1e48b553..34740187d77d9d 100644 --- a/src/plugins/data_view_field_editor/public/components/field_editor_flyout_content_container.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_editor_flyout_content_container.tsx @@ -11,18 +11,19 @@ import { DocLinksStart, NotificationsStart, CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; -import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import { BehaviorSubject } from 'rxjs'; import { DataViewField, DataView, DataPublicPluginStart, - RuntimeType, UsageCollectionStart, DataViewsPublicPluginStart, + FieldFormatsStart, + RuntimeType, } from '../shared_imports'; import type { Field, PluginStart, InternalFieldType } from '../types'; import { pluginName } from '../constants'; -import { deserializeField, getLinks, ApiService } from '../lib'; +import { getLinks, ApiService } from '../lib'; import { FieldEditorFlyoutContent, Props as FieldEditorFlyoutContentProps, @@ -32,7 +33,7 @@ import { FieldPreviewProvider } from './preview'; export interface Props { /** Handler for the "save" footer button */ - onSave: (field: DataViewField) => void; + onSave: (field: DataViewField[]) => void; /** Handler for the "cancel" footer button */ onCancel: () => void; onMounted?: FieldEditorFlyoutContentProps['onMounted']; @@ -43,7 +44,7 @@ export interface Props { /** The Kibana field type of the field to create or edit (default: "runtime") */ fieldTypeToProcess: InternalFieldType; /** Optional field to edit */ - fieldToEdit?: DataViewField; + fieldToEdit?: Field; /** Optional initial configuration for new field */ fieldToCreate?: Field; /** Services */ @@ -87,7 +88,16 @@ export const FieldEditorFlyoutContentContainer = ({ const { fields } = dataView; - const namesNotAllowed = useMemo(() => fields.map((fld) => fld.name), [fields]); + const namesNotAllowed = useMemo(() => { + const fieldNames = dataView.fields.map((fld) => fld.name); + const runtimeCompositeNames = Object.entries(dataView.getAllRuntimeFields()) + .filter(([, _runtimeField]) => _runtimeField.type === 'composite') + .map(([_runtimeFieldName]) => _runtimeFieldName); + return { + fields: fieldNames, + runtimeComposites: runtimeCompositeNames, + }; + }, [dataView]); const existingConcreteFields = useMemo(() => { const existing: Array<{ name: string; type: string }> = []; @@ -116,9 +126,12 @@ export const FieldEditorFlyoutContentContainer = ({ [apiService, search, notifications] ); - const saveField = useCallback( - async (updatedField: Field) => { - setIsSaving(true); + const updateRuntimeField = useCallback( + (updatedField: Field): DataViewField[] => { + const nameHasChanged = Boolean(fieldToEdit) && fieldToEdit!.name !== updatedField.name; + const typeHasChanged = Boolean(fieldToEdit) && fieldToEdit!.type !== updatedField.type; + const hasChangeToOrFromComposite = + typeHasChanged && (fieldToEdit!.type === 'composite' || updatedField.type === 'composite'); const { script } = updatedField; @@ -128,13 +141,14 @@ export const FieldEditorFlyoutContentContainer = ({ // eslint-disable-next-line no-empty } catch {} // rename an existing runtime field - if (fieldToEdit?.name && fieldToEdit.name !== updatedField.name) { - dataView.removeRuntimeField(fieldToEdit.name); + if (nameHasChanged || hasChangeToOrFromComposite) { + dataView.removeRuntimeField(fieldToEdit!.name); } dataView.addRuntimeField(updatedField.name, { type: updatedField.type as RuntimeType, script, + fields: updatedField.fields, }); } else { try { @@ -143,22 +157,54 @@ export const FieldEditorFlyoutContentContainer = ({ } catch {} } + return dataView.addRuntimeField(updatedField.name, updatedField); + }, + [fieldToEdit, dataView, fieldTypeToProcess, usageCollection] + ); + + const updateConcreteField = useCallback( + (updatedField: Field): DataViewField[] => { const editedField = dataView.getFieldByName(updatedField.name); + if (!editedField) { + throw new Error( + `Unable to find field named '${updatedField.name}' on index pattern '${dataView.title}'` + ); + } + + // Update custom label, popularity and format + dataView.setFieldCustomLabel(updatedField.name, updatedField.customLabel); + + editedField.count = updatedField.popularity || 0; + if (updatedField.format) { + dataView.setFieldFormat(updatedField.name, updatedField.format!); + } else { + dataView.deleteFieldFormat(updatedField.name); + } + + return [editedField]; + }, + [dataView] + ); + + const saveField = useCallback( + async (updatedField: Field) => { try { - if (!editedField) { - throw new Error( - `Unable to find field named '${updatedField.name}' on index pattern '${dataView.title}'` - ); - } + usageCollection.reportUiCounter( + pluginName, + METRIC_TYPE.COUNT, + fieldTypeToProcess === 'runtime' ? 'save_runtime' : 'save_concrete' + ); + // eslint-disable-next-line no-empty + } catch {} - dataView.setFieldCustomLabel(updatedField.name, updatedField.customLabel); - editedField.count = updatedField.popularity || 0; - if (updatedField.format) { - dataView.setFieldFormat(updatedField.name, updatedField.format); - } else { - dataView.deleteFieldFormat(updatedField.name); - } + setIsSaving(true); + + try { + const editedFields: DataViewField[] = + fieldTypeToProcess === 'runtime' + ? updateRuntimeField(updatedField) + : updateConcreteField(updatedField as Field); const afterSave = () => { const message = i18n.translate('indexPatternFieldEditor.deleteField.savedHeader', { @@ -167,17 +213,15 @@ export const FieldEditorFlyoutContentContainer = ({ }); notifications.toasts.addSuccess(message); setIsSaving(false); - onSave(editedField); + onSave(editedFields); }; - if (!dataView.isPersisted()) { - afterSave(); - return; + if (dataView.isPersisted()) { + await dataViews.updateSavedObject(dataView); } + afterSave(); - await dataViews.updateSavedObject(dataView).then(() => { - afterSave(); - }); + setIsSaving(false); } catch (e) { const title = i18n.translate('indexPatternFieldEditor.save.errorTitle', { defaultMessage: 'Failed to save field changes', @@ -192,7 +236,8 @@ export const FieldEditorFlyoutContentContainer = ({ dataViews, notifications, fieldTypeToProcess, - fieldToEdit?.name, + updateConcreteField, + updateRuntimeField, usageCollection, ] ); @@ -208,6 +253,8 @@ export const FieldEditorFlyoutContentContainer = ({ fieldFormats={fieldFormats} namesNotAllowed={namesNotAllowed} existingConcreteFields={existingConcreteFields} + fieldName$={new BehaviorSubject(fieldToEdit?.name || '')} + subfields$={new BehaviorSubject(fieldToEdit?.fields)} > diff --git a/src/plugins/data_view_field_editor/public/components/field_format_editor/format_editor.tsx b/src/plugins/data_view_field_editor/public/components/field_format_editor/format_editor.tsx index 4211047878cca9..7058b04b090531 100644 --- a/src/plugins/data_view_field_editor/public/components/field_format_editor/format_editor.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_format_editor/format_editor.tsx @@ -23,7 +23,7 @@ export interface FormatEditorProps { onError: (error?: string) => void; } -interface FormatEditorState { +export interface FormatEditorState { EditorComponent: LazyExoticComponent | null; fieldFormatId?: string; } diff --git a/src/plugins/data_view_field_editor/public/components/field_format_editor/index.ts b/src/plugins/data_view_field_editor/public/components/field_format_editor/index.ts index 0c23c8de616cfe..2ae6b3149dc5f6 100644 --- a/src/plugins/data_view_field_editor/public/components/field_format_editor/index.ts +++ b/src/plugins/data_view_field_editor/public/components/field_format_editor/index.ts @@ -8,4 +8,6 @@ export type { FormatSelectEditorProps } from './field_format_editor'; export { FormatSelectEditor } from './field_format_editor'; +export type { FormatEditorState } from './format_editor'; +export type { Sample } from './types'; export * from './editors'; diff --git a/src/plugins/data_view_field_editor/public/components/preview/field_preview.tsx b/src/plugins/data_view_field_editor/public/components/preview/field_preview.tsx index 05339e6473b6b9..f331e62bd70164 100644 --- a/src/plugins/data_view_field_editor/public/components/preview/field_preview.tsx +++ b/src/plugins/data_view_field_editor/public/components/preview/field_preview.tsx @@ -27,6 +27,7 @@ export const FieldPreview = () => { params: { value: { name, script, format }, }, + isLoadingPreview, fields, error, documents: { fetchDocError }, @@ -36,15 +37,15 @@ export const FieldPreview = () => { // To show the preview we at least need a name to be defined, the script or the format // and an first response from the _execute API - const isEmptyPromptVisible = - name === null && script === null && format === null - ? true - : // If we have some result from the _execute API call don't show the empty prompt - Boolean(error) || fields.length > 0 - ? false - : name === null && format === null - ? true - : false; + let isEmptyPromptVisible = false; + const noParamDefined = name === null && script === null && format === null; + const haveResultFromPreview = error !== null || fields.length > 0; + + if (noParamDefined) { + isEmptyPromptVisible = true; + } else if (!haveResultFromPreview && !isLoadingPreview && name === null && format === null) { + isEmptyPromptVisible = true; + } const doRenderListOfFields = fetchDocError === null; const showWarningPreviewNotAvailable = isPreviewAvailable === false && fetchDocError === null; @@ -58,13 +59,13 @@ export const FieldPreview = () => { return null; } - const [field] = fields; - return (
    -
  • - -
  • + {fields.map((field, i) => ( +
  • + +
  • + ))}
); }; diff --git a/src/plugins/data_view_field_editor/public/components/preview/field_preview_context.tsx b/src/plugins/data_view_field_editor/public/components/preview/field_preview_context.tsx index 127badffc826d0..6d6c38f8dfc61d 100644 --- a/src/plugins/data_view_field_editor/public/components/preview/field_preview_context.tsx +++ b/src/plugins/data_view_field_editor/public/components/preview/field_preview_context.tsx @@ -21,6 +21,8 @@ import useDebounce from 'react-use/lib/useDebounce'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; import { castEsToKbnFieldTypeName } from '@kbn/field-types'; +import { BehaviorSubject } from 'rxjs'; +import { RuntimePrimitiveTypes } from '../../shared_imports'; import { parseEsError } from '../../lib/runtime_field_validation'; import { useFieldEditorContext } from '../field_editor_context'; @@ -33,6 +35,7 @@ import type { EsDocument, ScriptErrorCodes, FetchDocError, + FieldPreview, } from './types'; const fieldPreviewContext = createContext(undefined); @@ -44,6 +47,7 @@ const defaultParams: Params = { document: null, type: null, format: null, + parentName: null, }; export const defaultValueFormatter = (value: unknown) => { @@ -51,6 +55,14 @@ export const defaultValueFormatter = (value: unknown) => { return renderToString(<>{content}); }; +export const valueTypeToSelectedType = (value: unknown): RuntimePrimitiveTypes => { + const valueType = typeof value; + if (valueType === 'string') return 'keyword'; + if (valueType === 'number') return 'double'; + if (valueType === 'boolean') return 'boolean'; + return 'keyword'; +}; + export const FieldPreviewProvider: FunctionComponent = ({ children }) => { const previewCount = useRef(0); @@ -75,13 +87,18 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { api: { getFieldPreview }, }, fieldFormats, + fieldName$, } = useFieldEditorContext(); + const fieldPreview$ = useRef(new BehaviorSubject(undefined)); + /** Response from the Painless _execute API */ const [previewResponse, setPreviewResponse] = useState<{ fields: Context['fields']; error: Context['error']; }>({ fields: [], error: null }); + const [initialPreviewComplete, setInitialPreviewComplete] = useState(false); + /** Possible error while fetching sample documents */ const [fetchDocError, setFetchDocError] = useState(null); /** The parameters required for the Painless _execute API */ @@ -126,7 +143,7 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { isPreviewAvailable = false; } - const { name, document, script, format, type } = params; + const { name, document, script, format, type, parentName } = params; const updateParams: Context['params']['update'] = useCallback((updated) => { setParams((prev) => ({ ...prev, ...updated })); @@ -314,12 +331,67 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { [dataView, search] ); + const updateSingleFieldPreview = useCallback( + (fieldName: string, values: unknown[]) => { + const [value] = values; + const formattedValue = valueFormatter(value); + + setPreviewResponse({ + fields: [{ key: fieldName, value, formattedValue }], + error: null, + }); + }, + [valueFormatter] + ); + + const updateCompositeFieldPreview = useCallback( + (compositeValues: Record) => { + const updatedFieldsInScript: string[] = []; + // if we're displaying a composite subfield, filter results + const filterSubfield = parentName ? (field: FieldPreview) => field.key === name : () => true; + + const fields = Object.entries(compositeValues) + .map(([key, values]) => { + // The Painless _execute API returns the composite field values under a map. + // Each of the key is prefixed with "composite_field." (e.g. "composite_field.field1: ['value']") + const { 1: fieldName } = key.split('composite_field.'); + updatedFieldsInScript.push(fieldName); + + const [value] = values; + const formattedValue = valueFormatter(value); + + return { + key: parentName + ? `${parentName ?? ''}.${fieldName}` + : `${fieldName$.getValue() ?? ''}.${fieldName}`, + value, + formattedValue, + type: valueTypeToSelectedType(value), + }; + }) + .filter(filterSubfield) + // ...and sort alphabetically + .sort((a, b) => a.key.localeCompare(b.key)); + + fieldPreview$.current.next(fields); + setPreviewResponse({ + fields, + error: null, + }); + }, + [valueFormatter, parentName, name, fieldPreview$, fieldName$] + ); + const updatePreview = useCallback(async () => { - if (scriptEditorValidation.isValidating) { + // don't prevent rendering if we're working with a composite subfield (has parentName) + if (!parentName && scriptEditorValidation.isValidating) { return; } - if (!allParamsDefined || !hasSomeParamsChanged || scriptEditorValidation.isValid === false) { + if ( + !parentName && + (!allParamsDefined || !hasSomeParamsChanged || scriptEditorValidation.isValid === false) + ) { setIsLoadingPreview(false); return; } @@ -332,11 +404,13 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { const currentApiCall = ++previewCount.current; + const previewScript = (parentName && dataView.getRuntimeField(parentName)?.script) || script!; + const response = await getFieldPreview({ index: currentDocIndex, document: document!, - context: `${type!}_field` as PainlessExecuteContext, - script: script!, + context: (parentName ? 'composite_field' : `${type!}_field`) as PainlessExecuteContext, + script: previewScript, }); if (currentApiCall !== previewCount.current) { @@ -363,33 +437,41 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { if (error) { setPreviewResponse({ - fields: [{ key: name ?? '', value: '', formattedValue: defaultValueFormatter('') }], + fields: [ + { + key: name ?? '', + value: '', + formattedValue: defaultValueFormatter(''), + }, + ], error: { code: 'PAINLESS_SCRIPT_ERROR', error: parseEsError(error) }, }); } else { - const [value] = values; - const formattedValue = valueFormatter(value); - - setPreviewResponse({ - fields: [{ key: name!, value, formattedValue }], - error: null, - }); + if (!Array.isArray(values)) { + updateCompositeFieldPreview(values); + } else { + updateSingleFieldPreview(name!, values); + } } } + setInitialPreviewComplete(true); setIsLoadingPreview(false); }, [ name, type, script, + parentName, + dataView, document, currentDocId, getFieldPreview, notifications.toasts, - valueFormatter, allParamsDefined, scriptEditorValidation, hasSomeParamsChanged, + updateSingleFieldPreview, + updateCompositeFieldPreview, currentDocIndex, ]); @@ -428,8 +510,10 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { () => ({ fields: previewResponse.fields, error: previewResponse.error, + fieldPreview$: fieldPreview$.current, isPreviewAvailable, isLoadingPreview, + initialPreviewComplete, params: { value: params, update: updateParams, @@ -470,6 +554,7 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { }), [ previewResponse, + fieldPreview$, fetchDocError, params, isPreviewAvailable, @@ -489,6 +574,7 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { from, reset, pinnedFields, + initialPreviewComplete, ] ); @@ -539,20 +625,61 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { */ useEffect(() => { setPreviewResponse((prev) => { - const { - fields: { 0: field }, - } = prev; + const { fields } = prev; + + let updatedFields: Context['fields'] = fields.map((field) => { + let key = name ?? ''; + + if (type === 'composite') { + // restore initial key segement (the parent name), which was not returned + const { 1: fieldName } = field.key.split('.'); + key = `${name ?? ''}.${fieldName}`; + } + + return { + ...field, + key, + }; + }); + + // If the user has entered a name but not yet any script we will display + // the field in the preview with just the name + if (updatedFields.length === 0 && name !== null) { + updatedFields = [ + { key: name, value: undefined, formattedValue: undefined, type: undefined }, + ]; + } - const nextValue = - script === null && Boolean(document) - ? get(document, name ?? '') // When there is no script we read the value from _source - : field?.value; + return { + ...prev, + fields: updatedFields, + }; + }); + }, [name, type, parentName]); - const formattedValue = valueFormatter(nextValue); + /** + * Whenever the format changes we immediately update the preview + */ + useEffect(() => { + setPreviewResponse((prev) => { + const { fields } = prev; return { ...prev, - fields: [{ ...field, key: name ?? '', value: nextValue, formattedValue }], + fields: fields.map((field) => { + const nextValue = + script === null && Boolean(document) + ? get(document, name ?? '') // When there is no script we try to read the value from _source + : field?.value; + + const formattedValue = valueFormatter(nextValue); + + return { + ...field, + value: nextValue, + formattedValue, + }; + }), }; }); }, [name, script, document, valueFormatter]); diff --git a/src/plugins/data_view_field_editor/public/components/preview/types.ts b/src/plugins/data_view_field_editor/public/components/preview/types.ts index 761c1db2094daa..881d512159516c 100644 --- a/src/plugins/data_view_field_editor/public/components/preview/types.ts +++ b/src/plugins/data_view_field_editor/public/components/preview/types.ts @@ -6,9 +6,14 @@ * Side Public License, v 1. */ -import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; import React from 'react'; -import type { RuntimeField, RuntimeType } from '../../shared_imports'; +import { BehaviorSubject } from 'rxjs'; +import type { + RuntimeType, + RuntimeField, + SerializedFieldFormat, + RuntimePrimitiveTypes, +} from '../../shared_imports'; import type { RuntimeFieldPainlessError } from '../../types'; export type From = 'cluster' | 'custom'; @@ -57,17 +62,39 @@ export interface Params { script: Required['script'] | null; format: SerializedFieldFormat | null; document: { [key: string]: unknown } | null; + // used for composite subfields + parentName: string | null; } export interface FieldPreview { key: string; value: unknown; formattedValue?: string; + type?: string; } +export interface FieldTypeInfo { + name: string; + type: string; +} + +export enum ChangeType { + UPSERT = 'upsert', + DELETE = 'delete', +} +export interface Change { + changeType: ChangeType; + type?: RuntimePrimitiveTypes; +} + +export type ChangeSet = Record; + export interface Context { fields: FieldPreview[]; + fieldPreview$: BehaviorSubject; error: PreviewError | null; + fieldTypeInfo?: FieldTypeInfo[]; + initialPreviewComplete: boolean; params: { value: Params; update: (updated: Partial) => void; diff --git a/src/plugins/data_view_field_editor/public/components/utils.ts b/src/plugins/data_view_field_editor/public/components/utils.ts new file mode 100644 index 00000000000000..0a7a8a3990cdc5 --- /dev/null +++ b/src/plugins/data_view_field_editor/public/components/utils.ts @@ -0,0 +1,17 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { RuntimeFieldSubFields, RuntimePrimitiveTypes } from '../shared_imports'; + +export const fieldTypeMapToRuntimeSpecFormat = ( + subfields: Record +): RuntimeFieldSubFields => + Object.entries(subfields).reduce((col, [name, type]) => { + col[name] = { type }; + return col; + }, {}); diff --git a/src/plugins/data_view_field_editor/public/index.ts b/src/plugins/data_view_field_editor/public/index.ts index 7f76210c2c6f04..95ec7e4dcfdc44 100644 --- a/src/plugins/data_view_field_editor/public/index.ts +++ b/src/plugins/data_view_field_editor/public/index.ts @@ -26,7 +26,14 @@ export type { PluginStart as IndexPatternFieldEditorStart, } from './types'; export { DefaultFormatEditor } from './components/field_format_editor/editors/default/default'; -export type { FieldFormatEditorFactory, FieldFormatEditor, FormatEditorProps } from './components'; +export type { + FieldFormatEditorFactory, + FieldFormatEditor, + DeleteFieldProviderProps, + FormatEditorProps, + FormatEditorState, + Sample, +} from './components'; export function plugin() { return new IndexPatternFieldEditorPlugin(); @@ -35,4 +42,4 @@ export function plugin() { // Expose types export type { FormatEditorServiceStart } from './service'; export type { OpenFieldEditorOptions } from './open_editor'; -export type { OpenFieldDeleteModalOptions } from './open_delete_modal'; +export type { OpenFieldDeleteModalOptions, DeleteCompositeSubfield } from './open_delete_modal'; diff --git a/src/plugins/data_view_field_editor/public/lib/serialization.ts b/src/plugins/data_view_field_editor/public/lib/serialization.ts index 82051eef176636..6cf43810892260 100644 --- a/src/plugins/data_view_field_editor/public/lib/serialization.ts +++ b/src/plugins/data_view_field_editor/public/lib/serialization.ts @@ -14,13 +14,17 @@ export const deserializeField = (dataView: DataView, field?: DataViewField): Fie return undefined; } + const primitiveType = field?.esTypes ? (field.esTypes[0] as RuntimeType) : ('keyword' as const); + const editType = field.runtimeField?.type === 'composite' ? 'composite' : primitiveType; + return { name: field.name, - type: field?.esTypes ? (field.esTypes[0] as RuntimeType) : ('keyword' as const), + type: editType, script: field.runtimeField ? field.runtimeField.script : undefined, customLabel: field.customLabel, popularity: field.count, format: dataView.getFormatterForFieldNoDefault(field.name)?.toJSON(), + fields: field.runtimeField?.fields, }; }; diff --git a/src/plugins/data_view_field_editor/public/open_delete_modal.tsx b/src/plugins/data_view_field_editor/public/open_delete_modal.tsx index 2703653138fe0f..b2c9bba61b98fc 100644 --- a/src/plugins/data_view_field_editor/public/open_delete_modal.tsx +++ b/src/plugins/data_view_field_editor/public/open_delete_modal.tsx @@ -21,11 +21,24 @@ import { CloseEditor } from './types'; import { DeleteFieldModal } from './components/confirm_modals/delete_field_modal'; import { removeFields } from './lib/remove_fields'; +/** + * Options for opening the field editor + */ export interface OpenFieldDeleteModalOptions { + /** + * Config for the delete modal + */ ctx: { dataView: DataView; }; + /** + * Callback fired when fields are deleted + * @param fieldNames - the names of the deleted fields + */ onDelete?: (fieldNames: string[]) => void; + /** + * Names of the fields to be deleted + */ fieldName: string | string[]; } @@ -35,13 +48,38 @@ interface Dependencies { usageCollection: UsageCollectionStart; } +/** + * Error throw when there's an attempt to directly delete a composite subfield + * @param fieldName - the name of the field to delete + */ +export class DeleteCompositeSubfield extends Error { + constructor(fieldName: string) { + super(`Field '${fieldName} cannot be deleted because it is a composite subfield.`); + } +} + export const getFieldDeleteModalOpener = ({ core, dataViews, usageCollection }: Dependencies) => (options: OpenFieldDeleteModalOptions): CloseEditor => { + if (typeof options.fieldName === 'string') { + const fieldToDelete = options.ctx.dataView.getFieldByName(options.fieldName); + // we can check for composite type since composite runtime field definitions themselves don't become fields + const doesBelongToCompositeField = fieldToDelete?.runtimeField?.type === 'composite'; + + if (doesBelongToCompositeField) { + throw new DeleteCompositeSubfield(options.fieldName); + } + } + const { overlays, notifications } = core; let overlayRef: OverlayRef | null = null; + /** + * Open the delete field modal + * @param Options for delete field modal + * @returns Function to close the delete field modal + */ const openDeleteModal = ({ onDelete, fieldName, diff --git a/src/plugins/data_view_field_editor/public/open_editor.tsx b/src/plugins/data_view_field_editor/public/open_editor.tsx index d938ae52642b71..6d3dbc6c3f4cd9 100644 --- a/src/plugins/data_view_field_editor/public/open_editor.tsx +++ b/src/plugins/data_view_field_editor/public/open_editor.tsx @@ -15,11 +15,11 @@ import type { ApiService } from './lib/api'; import type { DataPublicPluginStart, DataView, - DataViewField, + UsageCollectionStart, + RuntimeType, DataViewsPublicPluginStart, FieldFormatsStart, - RuntimeType, - UsageCollectionStart, + DataViewField, } from './shared_imports'; import { createKibanaReactContext, toMountPoint } from './shared_imports'; import type { CloseEditor, Field, InternalFieldType, PluginStart } from './types'; @@ -37,8 +37,9 @@ export interface OpenFieldEditorOptions { }; /** * action to take after field is saved + * @param field - the fields that were saved */ - onSave?: (field: DataViewField) => void; + onSave?: (field: DataViewField[]) => void; /** * field to edit, for existing field */ @@ -100,7 +101,7 @@ export const getFieldEditorOpener = } }; - const onSaveField = (updatedField: DataViewField) => { + const onSaveField = (updatedField: DataViewField[]) => { closeEditor(); if (onSave) { @@ -108,9 +109,27 @@ export const getFieldEditorOpener = } }; - const fieldToEdit = fieldNameToEdit ? dataView.getFieldByName(fieldNameToEdit) : undefined; + const getRuntimeField = (name: string) => { + const fld = dataView.getAllRuntimeFields()[name]; + return { + name, + runtimeField: fld, + isMapped: false, + esTypes: [], + type: undefined, + customLabel: undefined, + count: undefined, + spec: { + parentName: undefined, + }, + }; + }; + + const dataViewField = fieldNameToEdit + ? dataView.getFieldByName(fieldNameToEdit) || getRuntimeField(fieldNameToEdit) + : undefined; - if (fieldNameToEdit && !fieldToEdit) { + if (fieldNameToEdit && !dataViewField) { const err = i18n.translate('indexPatternFieldEditor.noSuchFieldName', { defaultMessage: "Field named '{fieldName}' not found on index pattern", values: { fieldName: fieldNameToEdit }, @@ -121,14 +140,42 @@ export const getFieldEditorOpener = const isNewRuntimeField = !fieldNameToEdit; const isExistingRuntimeField = - fieldToEdit && - fieldToEdit.runtimeField && - !fieldToEdit.isMapped && - // treat composite field instances as mapped fields for field editing purposes - fieldToEdit.runtimeField.type !== ('composite' as RuntimeType); + dataViewField && + dataViewField.runtimeField && + !dataViewField.isMapped && + // treat composite subfield instances as mapped fields for field editing purposes + (dataViewField.runtimeField.type !== ('composite' as RuntimeType) || !dataViewField.type); + const fieldTypeToProcess: InternalFieldType = isNewRuntimeField || isExistingRuntimeField ? 'runtime' : 'concrete'; + let field: Field | undefined; + if (dataViewField) { + if (isExistingRuntimeField && dataViewField.runtimeField!.type === 'composite') { + // Composite runtime subfield + const [compositeName] = fieldNameToEdit!.split('.'); + field = { + name: compositeName, + ...dataView.getRuntimeField(compositeName)!, + }; + } else if (isExistingRuntimeField) { + // Runtime field + field = { + name: fieldNameToEdit!, + ...dataView.getRuntimeField(fieldNameToEdit!)!, + }; + } else { + // Concrete field + field = { + name: fieldNameToEdit!, + type: (dataViewField?.esTypes ? dataViewField.esTypes[0] : 'keyword') as RuntimeType, + customLabel: dataViewField.customLabel, + popularity: dataViewField.count, + format: dataView.getFormatterForFieldNoDefault(fieldNameToEdit!)?.toJSON(), + parentName: dataViewField.spec.parentName, + }; + } + } overlayRef = overlays.openFlyout( toMountPoint( @@ -137,7 +184,7 @@ export const getFieldEditorOpener = onCancel={closeEditor} onMounted={onMounted} docLinks={docLinks} - fieldToEdit={fieldToEdit} + fieldToEdit={field} fieldToCreate={fieldToCreate} fieldTypeToProcess={fieldTypeToProcess} dataView={dataView} diff --git a/src/plugins/data_view_field_editor/public/shared_imports.ts b/src/plugins/data_view_field_editor/public/shared_imports.ts index 6d7c6b4222d50d..ff6621d64cbd1c 100644 --- a/src/plugins/data_view_field_editor/public/shared_imports.ts +++ b/src/plugins/data_view_field_editor/public/shared_imports.ts @@ -17,7 +17,14 @@ export type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; export type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; -export type { RuntimeType, RuntimeField } from '@kbn/data-views-plugin/common'; +export type { + RuntimeType, + RuntimeField, + RuntimeFieldSpec, + RuntimeFieldSubField, + RuntimeFieldSubFields, + RuntimePrimitiveTypes, +} from '@kbn/data-views-plugin/common'; export { KBN_FIELD_TYPES, ES_FIELD_TYPES } from '@kbn/data-plugin/common'; export { @@ -26,7 +33,7 @@ export { CodeEditor, } from '@kbn/kibana-react-plugin/public'; -export { FieldFormat } from '@kbn/field-formats-plugin/common'; +export type { FieldFormat, SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; export type { FormSchema, @@ -44,6 +51,7 @@ export { Form, UseField, useBehaviorSubject, + UseArray, } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; export { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; diff --git a/src/plugins/data_view_field_editor/public/types.ts b/src/plugins/data_view_field_editor/public/types.ts index 8ee47f20515a10..3688f79c16689f 100644 --- a/src/plugins/data_view_field_editor/public/types.ts +++ b/src/plugins/data_view_field_editor/public/types.ts @@ -17,8 +17,9 @@ import { DataViewsPublicPluginStart, FieldFormatsStart, RuntimeField, - RuntimeType, UsageCollectionStart, + RuntimeType, + SerializedFieldFormat, } from './shared_imports'; /** @@ -35,14 +36,27 @@ export interface PluginSetup { */ export interface PluginStart { /** - * method to open the data view field editor fly-out + * Method to open the data view field editor fly-out */ openEditor(options: OpenFieldEditorOptions): () => void; + /** + * Method to open the data view field delete fly-out + * @param options Configuration options for the fly-out + */ openDeleteModal(options: OpenFieldDeleteModalOptions): () => void; fieldFormatEditors: FormatEditorServiceStart['fieldFormatEditors']; + /** + * Convenience method for user permissions checks + */ userPermissions: { + /** + * Whether the user has permission to edit data views + */ editIndexPattern: () => boolean; }; + /** + * Context provider for delete runtime field modal + */ DeleteRuntimeFieldProvider: FunctionComponent; } @@ -62,33 +76,21 @@ export type InternalFieldType = 'concrete' | 'runtime'; * The data model for the field editor * @public */ -export interface Field { +export interface Field extends RuntimeField { /** * name / path used for the field */ name: FieldSpec['name']; /** - * ES type + * Name of parent field. Used for composite subfields */ - type: RuntimeType; - /** - * source of the runtime field script - */ - script?: RuntimeField['script']; - /** - * custom label for display - */ - customLabel?: FieldSpec['customLabel']; - /** - * custom popularity - */ - popularity?: number; - /** - * configuration of the field format - */ - format?: FieldSpec['format']; + parentName?: string; } +export interface FieldFormatConfig { + id: string; + params?: SerializedFieldFormat['params']; +} export interface EsRuntimeField { type: RuntimeType | string; script?: { diff --git a/src/plugins/data_view_field_editor/server/routes/field_preview.ts b/src/plugins/data_view_field_editor/server/routes/field_preview.ts index 6423694f76ec97..bee5fc0dbd1bef 100644 --- a/src/plugins/data_view_field_editor/server/routes/field_preview.ts +++ b/src/plugins/data_view_field_editor/server/routes/field_preview.ts @@ -24,6 +24,7 @@ const bodySchema = schema.object({ schema.literal('ip_field'), schema.literal('keyword_field'), schema.literal('long_field'), + schema.literal('composite_field'), ]), document: schema.object({}, { unknowns: 'allow' }), }); diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx index 3c9eeaa63109d6..208871f77022ae 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -20,13 +20,14 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import { DataView, DataViewField, RuntimeField } from '@kbn/data-views-plugin/public'; import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { SavedObjectRelation, SavedObjectManagementTypeInfo, } from '@kbn/saved-objects-management-plugin/public'; +import { pickBy } from 'lodash'; import { IndexPatternManagmentContext } from '../../types'; import { Tabs } from './tabs'; import { IndexHeader } from './index_header'; @@ -61,11 +62,17 @@ const securityDataView = i18n.translate( const securitySolution = 'security-solution'; +const getCompositeRuntimeFields = (dataView: DataView) => + pickBy(dataView.getAllRuntimeFields(), (fld) => fld.type === 'composite'); + export const EditIndexPattern = withRouter( ({ indexPattern, history, location }: EditIndexPatternProps) => { const { uiSettings, overlays, chrome, dataViews, IndexPatternEditor, savedObjectsManagement } = useKibana().services; const [fields, setFields] = useState(indexPattern.getNonScriptedFields()); + const [compositeRuntimeFields, setCompositeRuntimeFields] = useState< + Record + >(() => getCompositeRuntimeFields(indexPattern)); const [conflictedFields, setConflictedFields] = useState( indexPattern.fields.getAll().filter((field) => field.type === 'conflict') ); @@ -250,8 +257,10 @@ export const EditIndexPattern = withRouter( allowedTypes={allowedTypes} history={history} location={location} + compositeRuntimeFields={compositeRuntimeFields} refreshFields={() => { setFields(indexPattern.getNonScriptedFields()); + setCompositeRuntimeFields(getCompositeRuntimeFields(indexPattern)); }} /> {displayIndexPatternEditor} diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap index c054b42f51ac75..e06bcf99e3399b 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap @@ -93,11 +93,7 @@ exports[`Table render name 2`] = `   - This field exists on the data view only. - - } + content="This field exists on the data view only." title="Runtime field" type="indexRuntime" /> diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx index 1646ec0f24ed5b..ab6e82b301f012 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx @@ -34,7 +34,10 @@ import { DataView } from '@kbn/data-views-plugin/public'; import { IndexedFieldItem } from '../../types'; export const showDelete = (field: IndexedFieldItem) => - !field.isMapped && field.isUserEditable && field.runtimeField?.type !== 'composite'; + // runtime fields that aren't composite subfields + (!field.isMapped && field.isUserEditable && field.runtimeField?.type !== 'composite') || + // composite runtime field definitions + (field.runtimeField?.type === 'composite' && field.type === 'composite'); // localized labels const additionalInfoAriaLabel = i18n.translate( @@ -161,10 +164,29 @@ const labelDescription = i18n.translate( { defaultMessage: 'A custom label for the field.' } ); -const runtimeIconTipTitle = i18n.translate( - 'indexPatternManagement.editIndexPattern.fields.table.runtimeIconTipTitle', - { defaultMessage: 'Runtime field' } -); +function runtimeIconTipTitle(fld: IndexedFieldItem) { + // composite runtime fields + if (fld.runtimeField?.type === 'composite') { + // subfields definitions + if (fld.type !== 'composite') { + return i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.table.runtimeIconTipTitleCompositeSubfield', + { defaultMessage: 'Composite runtime subfield' } + ); + // composite definitions + } else { + return i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.table.runtimeIconTipTitleComposite', + { defaultMessage: 'Composite runtime field' } + ); + } + } + + return i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.table.runtimeIconTipTitle', + { defaultMessage: 'Runtime field' } + ); +} const runtimeIconTipText = i18n.translate( 'indexPatternManagement.editDataView.fields.table.runtimeIconTipText', @@ -180,7 +202,7 @@ interface IndexedFieldProps { indexPattern: DataView; items: IndexedFieldItem[]; editField: (field: IndexedFieldItem) => void; - deleteField: (fieldName: string) => void; + deleteField: (fieldName: string[]) => void; openModal: OverlayModalStart['open']; theme: ThemeServiceStart; } @@ -229,8 +251,8 @@ export const renderFieldName = (field: IndexedFieldItem, timeFieldName?: string)   {runtimeIconTipText}} + title={runtimeIconTipTitle(field)} + content={runtimeIconTipText} /> ) : null} @@ -454,7 +476,16 @@ export class Table extends PureComponent { name: deleteLabel, description: deleteDescription, icon: 'trash', - onClick: (field) => deleteField(field.name), + onClick: (field) => { + const toDelete = [field.name]; + if (field.spec?.runtimeField?.fields) { + const childFieldNames = Object.keys(field.spec.runtimeField.fields).map( + (key) => `${field.name}.${key}` + ); + toDelete.push(...childFieldNames); + } + deleteField(toDelete); + }, type: 'icon', 'data-test-subj': 'deleteField', available: showDelete, diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx index 4cc5792ce756bc..01ffc9377d8b21 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx @@ -29,7 +29,7 @@ jest.mock('./components/table', () => ({ const helpers = { editField: (fieldName: string) => {}, - deleteField: (fieldName: string) => {}, + deleteField: (fieldName: string[]) => {}, // getFieldInfo handles non rollups as well getFieldInfo, }; @@ -118,6 +118,7 @@ describe('IndexedFieldsTable', () => { indexedFieldTypeFilter={[]} schemaFieldTypeFilter={[]} fieldFilter="" + compositeRuntimeFields={{}} {...mockedServices} /> ); @@ -140,6 +141,7 @@ describe('IndexedFieldsTable', () => { indexedFieldTypeFilter={[]} schemaFieldTypeFilter={[]} fieldFilter="" + compositeRuntimeFields={{}} {...mockedServices} /> ); @@ -163,6 +165,7 @@ describe('IndexedFieldsTable', () => { indexedFieldTypeFilter={[]} schemaFieldTypeFilter={[]} fieldFilter="" + compositeRuntimeFields={{}} {...mockedServices} /> ); @@ -186,6 +189,7 @@ describe('IndexedFieldsTable', () => { indexedFieldTypeFilter={[]} schemaFieldTypeFilter={[]} fieldFilter="" + compositeRuntimeFields={{}} {...mockedServices} /> ); @@ -210,6 +214,7 @@ describe('IndexedFieldsTable', () => { indexedFieldTypeFilter={[]} schemaFieldTypeFilter={[]} fieldFilter="" + compositeRuntimeFields={{}} {...mockedServices} /> ); diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx index e4326439e574a1..2a06f0c88b54f3 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -9,7 +9,7 @@ import React, { Component } from 'react'; import { createSelector } from 'reselect'; import { OverlayStart, ThemeServiceStart } from '@kbn/core/public'; -import { DataViewField, DataView } from '@kbn/data-views-plugin/public'; +import { DataViewField, DataView, RuntimeField } from '@kbn/data-views-plugin/public'; import { Table } from './components/table'; import { IndexedFieldItem } from './types'; @@ -21,13 +21,14 @@ interface IndexedFieldsTableProps { schemaFieldTypeFilter: string[]; helpers: { editField: (fieldName: string) => void; - deleteField: (fieldName: string) => void; + deleteField: (fieldName: string[]) => void; getFieldInfo: (indexPattern: DataView, field: DataViewField) => string[]; }; fieldWildcardMatcher: (filters: string[] | undefined) => (val: string) => boolean; userEditPermission: boolean; openModal: OverlayStart['openModal']; theme: ThemeServiceStart; + compositeRuntimeFields: Record; } interface IndexedFieldsTableState { @@ -42,14 +43,23 @@ export class IndexedFieldsTable extends Component< super(props); this.state = { - fields: this.mapFields(this.props.fields), + fields: [ + ...this.mapCompositeRuntimeFields(this.props.compositeRuntimeFields), + ...this.mapFields(this.props.fields), + ], }; } UNSAFE_componentWillReceiveProps(nextProps: IndexedFieldsTableProps) { - if (nextProps.fields !== this.props.fields) { + if ( + nextProps.fields !== this.props.fields || + nextProps.compositeRuntimeFields !== this.props.compositeRuntimeFields + ) { this.setState({ - fields: this.mapFields(nextProps.fields), + fields: [ + ...this.mapCompositeRuntimeFields(nextProps.compositeRuntimeFields), + ...this.mapFields(nextProps.fields), + ], }); } } @@ -57,7 +67,8 @@ export class IndexedFieldsTable extends Component< mapFields(fields: DataViewField[]): IndexedFieldItem[] { const { indexPattern, fieldWildcardMatcher, helpers, userEditPermission } = this.props; const sourceFilters = - indexPattern.sourceFilters && indexPattern.sourceFilters.map((f) => f.value); + indexPattern.sourceFilters && + indexPattern.sourceFilters.map((f: Record) => f.value); const fieldWildcardMatch = fieldWildcardMatcher(sourceFilters || []); return ( @@ -80,6 +91,46 @@ export class IndexedFieldsTable extends Component< ); } + mapCompositeRuntimeFields( + compositeRuntimeFields: Record + ): IndexedFieldItem[] { + const { indexPattern, fieldWildcardMatcher, userEditPermission } = this.props; + const sourceFilters = + indexPattern.sourceFilters && + indexPattern.sourceFilters.map((f: Record) => f.value); + const fieldWildcardMatch = fieldWildcardMatcher(sourceFilters || []); + + return Object.entries(compositeRuntimeFields).map(([name, fld]) => { + return { + spec: { + searchable: false, + aggregatable: false, + name, + type: 'composite', + runtimeField: { + type: 'composite', + script: fld.script, + fields: fld.fields, + }, + }, + name, + type: 'composite', + kbnType: '', + displayName: name, + excluded: fieldWildcardMatch ? fieldWildcardMatch(name) : false, + info: [], + isMapped: false, + isUserEditable: userEditPermission, + hasRuntime: true, + runtimeField: { + type: 'composite', + script: fld.script, + fields: fld.fields, + }, + }; + }); + } + getFilteredFields = createSelector( (state: IndexedFieldsTableState) => state.fields, (_state: IndexedFieldsTableState, props: IndexedFieldsTableProps) => props.fieldFilter, @@ -135,14 +186,13 @@ export class IndexedFieldsTable extends Component< render() { const { indexPattern } = this.props; const fields = this.getFilteredFields(this.state, this.props); - return (
this.props.helpers.editField(field.name)} - deleteField={(fieldName) => this.props.helpers.deleteField(fieldName)} + deleteField={(fieldNames) => this.props.helpers.deleteField(fieldNames)} openModal={this.props.openModal} theme={this.props.theme} /> diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx index a4b87110521d71..075869ab6fdc22 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx @@ -29,6 +29,7 @@ import { DataViewField, DataViewsPublicPluginStart, META_FIELDS, + RuntimeField, } from '@kbn/data-views-plugin/public'; import { SavedObjectRelation, @@ -57,6 +58,7 @@ interface TabsProps extends Pick { refreshFields: () => void; relationships: SavedObjectRelation[]; allowedTypes: SavedObjectManagementTypeInfo[]; + compositeRuntimeFields: Record; } interface FilterItems { @@ -144,6 +146,7 @@ export function Tabs({ refreshFields, relationships, allowedTypes, + compositeRuntimeFields, }: TabsProps) { const { uiSettings, @@ -462,6 +465,7 @@ export function Tabs({ {(deleteField) => ( { }, fields: { a: { - type: 'keyword' as RuntimeTypeExceptComposite, + type: 'keyword' as RuntimePrimitiveTypes, }, b: { - type: 'long' as RuntimeTypeExceptComposite, + type: 'long' as RuntimePrimitiveTypes, }, }, }; diff --git a/src/plugins/data_views/common/data_views/data_views.ts b/src/plugins/data_views/common/data_views/data_views.ts index 7c5e9314a153e8..892df9b312e86d 100644 --- a/src/plugins/data_views/common/data_views/data_views.ts +++ b/src/plugins/data_views/common/data_views/data_views.ts @@ -784,7 +784,8 @@ export class DataViewsService { const addRuntimeFieldToSpecFields = ( name: string, fieldType: RuntimeType, - runtimeField: RuntimeFieldSpec + runtimeField: RuntimeFieldSpec, + parentName?: string ) => { spec[name] = { name, @@ -797,6 +798,10 @@ export class DataViewsService { customLabel: fieldAttrs?.[name]?.customLabel, count: fieldAttrs?.[name]?.count, }; + + if (parentName) { + spec[name].parentName = parentName; + } }; // CREATE RUNTIME FIELDS @@ -804,7 +809,7 @@ export class DataViewsService { // For composite runtime field we add the subFields, **not** the composite if (runtimeField.type === 'composite') { Object.entries(runtimeField.fields!).forEach(([subFieldName, subField]) => { - addRuntimeFieldToSpecFields(`${name}.${subFieldName}`, subField.type, runtimeField); + addRuntimeFieldToSpecFields(`${name}.${subFieldName}`, subField.type, runtimeField, name); }); } else { addRuntimeFieldToSpecFields(name, runtimeField.type, runtimeField); diff --git a/src/plugins/data_views/common/data_views/utils.ts b/src/plugins/data_views/common/data_views/utils.ts index 31a6b573ff2b3c..a5d0447f793392 100644 --- a/src/plugins/data_views/common/data_views/utils.ts +++ b/src/plugins/data_views/common/data_views/utils.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { RuntimeField, RuntimeFieldSpec, RuntimeTypeExceptComposite } from '../types'; +import type { RuntimeField, RuntimeFieldSpec, RuntimePrimitiveTypes } from '../types'; export const removeFieldAttrs = (runtimeField: RuntimeField): RuntimeFieldSpec => { const { type, script, fields } = runtimeField; @@ -14,7 +14,7 @@ export const removeFieldAttrs = (runtimeField: RuntimeField): RuntimeFieldSpec = fields: Object.entries(fields).reduce((col, [fieldName, field]) => { col[fieldName] = { type: field.type }; return col; - }, {} as Record), + }, {} as Record), }; return { diff --git a/src/plugins/data_views/common/index.ts b/src/plugins/data_views/common/index.ts index ffeeb069d19121..5f7b3a544db9df 100644 --- a/src/plugins/data_views/common/index.ts +++ b/src/plugins/data_views/common/index.ts @@ -27,9 +27,11 @@ export { export type { FieldFormatMap, RuntimeType, + RuntimePrimitiveTypes, RuntimeField, RuntimeFieldSpec, RuntimeFieldSubField, + RuntimeFieldSubFields, DataViewAttributes, OnNotification, OnError, @@ -46,7 +48,6 @@ export type { DataViewSpec, SourceFilter, HasDataService, - RuntimeTypeExceptComposite, RuntimeFieldBase, FieldConfiguration, SavedObjectsClientCommonFindArgs, diff --git a/src/plugins/data_views/common/types.ts b/src/plugins/data_views/common/types.ts index ec0aeb081124eb..54c8c61635f630 100644 --- a/src/plugins/data_views/common/types.ts +++ b/src/plugins/data_views/common/types.ts @@ -23,18 +23,14 @@ export type { SavedObject }; export type FieldFormatMap = Record; /** - * Runtime field - type of value returned - * @public + * Runtime field types */ - export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; /** - * Primitive runtime field types - * @public + * Runtime field primitive types - excluding composite */ - -export type RuntimeTypeExceptComposite = Exclude; +export type RuntimePrimitiveTypes = Exclude; /** * Runtime field definition @@ -61,11 +57,14 @@ export type RuntimeFieldBase = { * The RuntimeField that will be sent in the ES Query "runtime_mappings" object */ export type RuntimeFieldSpec = RuntimeFieldBase & { + /** + * Composite subfields + */ fields?: Record< string, { // It is not recursive, we can't create a composite inside a composite. - type: RuntimeTypeExceptComposite; + type: RuntimePrimitiveTypes; } >; }; @@ -98,18 +97,18 @@ export interface RuntimeField extends RuntimeFieldBase, FieldConfiguration { /** * Subfields of composite field */ - fields?: Record; + fields?: RuntimeFieldSubFields; } +export type RuntimeFieldSubFields = Record; + /** * Runtime field composite subfield * @public */ export interface RuntimeFieldSubField extends FieldConfiguration { - /** - * Type of runtime field, can only be primitive type - */ - type: RuntimeTypeExceptComposite; + // It is not recursive, we can't create a composite inside a composite. + type: RuntimePrimitiveTypes; } /** @@ -448,6 +447,10 @@ export type FieldSpec = DataViewFieldBase & { * Is this field in the mapping? False if a scripted or runtime field defined on the data view. */ isMapped?: boolean; + /** + * Name of parent field for composite runtime field subfields. + */ + parentName?: string; }; export type DataViewFieldMap = Record; diff --git a/src/plugins/data_views/public/index.ts b/src/plugins/data_views/public/index.ts index cf48aaee81fd04..f886d60696b8aa 100644 --- a/src/plugins/data_views/public/index.ts +++ b/src/plugins/data_views/public/index.ts @@ -20,6 +20,7 @@ export type { FieldSpec, DataViewAttributes, SavedObjectsClientCommon, + RuntimeField, } from '../common'; export { DataViewField, diff --git a/src/plugins/discover/public/__mocks__/data_views.ts b/src/plugins/discover/public/__mocks__/data_views.ts index e16f16b7a87e9a..f1509b323e098d 100644 --- a/src/plugins/discover/public/__mocks__/data_views.ts +++ b/src/plugins/discover/public/__mocks__/data_views.ts @@ -8,6 +8,8 @@ import { DataViewsContract } from '@kbn/data-views-plugin/public'; import { dataViewMock } from './data_view'; +import { dataViewComplexMock } from './data_view_complex'; +import { dataViewWithTimefieldMock } from './data_view_with_timefield'; export const dataViewsMock = { getCache: async () => { @@ -21,4 +23,7 @@ export const dataViewsMock = { } }, updateSavedObject: jest.fn(), + getIdsWithTitle: jest.fn(() => { + return Promise.resolve([dataViewMock, dataViewComplexMock, dataViewWithTimefieldMock]); + }), } as unknown as jest.Mocked; diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index a6c95405ccd5da..5a9287d6c768c0 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -25,9 +25,12 @@ import { TopNavMenu } from '@kbn/navigation-plugin/public'; import { FORMATS_UI_SETTINGS } from '@kbn/field-formats-plugin/common'; import { LocalStorageMock } from './local_storage_mock'; import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks'; +import { dataViewsMock } from './data_views'; const dataPlugin = dataPluginMock.createStartContract(); const expressionsPlugin = expressionsPluginMock.createStartContract(); +dataPlugin.query.filterManager.getFilters = jest.fn(() => []); + export const discoverServiceMock = { core: coreMock.createStart(), chrome: chromeServiceMock.createStartContract(), @@ -52,6 +55,9 @@ export const discoverServiceMock = { }, fieldFormats: fieldFormatsMock, filterManager: dataPlugin.query.filterManager, + inspector: { + open: jest.fn(), + }, uiSettings: { get: jest.fn((key: string) => { if (key === 'fields:popularLimit') { @@ -114,4 +120,5 @@ export const discoverServiceMock = { }, expressions: expressionsPlugin, savedObjectsTagging: {}, + dataViews: dataViewsMock, } as unknown as DiscoverServices; diff --git a/src/plugins/discover/public/application/context/context_app.tsx b/src/plugins/discover/public/application/context/context_app.tsx index f44103f70b8ddf..b7e3f834781ccb 100644 --- a/src/plugins/discover/public/application/context/context_app.tsx +++ b/src/plugins/discover/public/application/context/context_app.tsx @@ -57,7 +57,7 @@ export const ContextApp = ({ dataView, anchorId }: ContextAppProps) => { /** * Context app state */ - const { appState, globalState, setAppState } = useContextAppState({ services }); + const { appState, globalState, setAppState } = useContextAppState({ services, dataView }); const prevAppState = useRef(); const prevGlobalState = useRef({ filters: [] }); diff --git a/src/plugins/discover/public/application/context/hooks/use_context_app_state.ts b/src/plugins/discover/public/application/context/hooks/use_context_app_state.ts index 594ef3d3ffd4c8..db6ba390f67a76 100644 --- a/src/plugins/discover/public/application/context/hooks/use_context_app_state.ts +++ b/src/plugins/discover/public/application/context/hooks/use_context_app_state.ts @@ -5,13 +5,20 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { DataView } from '@kbn/data-views-plugin/common'; import { useEffect, useMemo, useState } from 'react'; import { CONTEXT_DEFAULT_SIZE_SETTING } from '../../../../common'; import { DiscoverServices } from '../../../build_services'; import { AppState, getState, GlobalState } from '../services/context_state'; -export function useContextAppState({ services }: { services: DiscoverServices }) { +export function useContextAppState({ + services, + dataView, +}: { + services: DiscoverServices; + dataView: DataView; +}) { const { uiSettings: config, history, core } = services; const stateContainer = useMemo(() => { @@ -22,8 +29,9 @@ export function useContextAppState({ services }: { services: DiscoverServices }) toasts: core.notifications.toasts, uiSettings: config, data: services.data, + dataView, }); - }, [config, history, core.notifications.toasts, services.data]); + }, [config, history, core.notifications.toasts, services.data, dataView]); const [appState, setAppState] = useState(stateContainer.appState.getState()); const [globalState, setGlobalState] = useState( diff --git a/src/plugins/discover/public/application/context/services/context_state.test.ts b/src/plugins/discover/public/application/context/services/context_state.test.ts index a420b8d08e0b6d..ae1916158f85e2 100644 --- a/src/plugins/discover/public/application/context/services/context_state.test.ts +++ b/src/plugins/discover/public/application/context/services/context_state.test.ts @@ -14,6 +14,7 @@ import { FilterManager } from '@kbn/data-plugin/public'; import { coreMock } from '@kbn/core/public/mocks'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; import { discoverServiceMock } from '../../../__mocks__/services'; +import { dataViewMock } from '../../../__mocks__/data_view'; discoverServiceMock.data.query.filterManager.getAppFilters = jest.fn(() => []); discoverServiceMock.data.query.filterManager.getGlobalFilters = jest.fn(() => []); @@ -34,6 +35,7 @@ describe('Test Discover Context State', () => { (key === SEARCH_FIELDS_FROM_SOURCE ? true : ['_source']) as unknown as T, } as IUiSettingsClient, data: discoverServiceMock.data, + dataView: dataViewMock, }); state.startSync(); }); @@ -131,7 +133,7 @@ describe('Test Discover Context State', () => { "query": "jpg", }, "type": "phrase", - "value": [Function], + "value": undefined, }, "query": Object { "match_phrase": Object { @@ -155,7 +157,7 @@ describe('Test Discover Context State', () => { "query": "png", }, "type": "phrase", - "value": [Function], + "value": undefined, }, "query": Object { "match_phrase": Object { diff --git a/src/plugins/discover/public/application/context/services/context_state.ts b/src/plugins/discover/public/application/context/services/context_state.ts index 9739b5bc0fde49..8b41152496ec9f 100644 --- a/src/plugins/discover/public/application/context/services/context_state.ts +++ b/src/plugins/discover/public/application/context/services/context_state.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { cloneDeep, isEqual } from 'lodash'; +import { isEqual } from 'lodash'; import { History } from 'history'; import { NotificationsStart, IUiSettingsClient } from '@kbn/core/public'; import { Filter, compareFilters, COMPARE_ALL_OPTIONS, FilterStateStore } from '@kbn/es-query'; @@ -19,6 +19,8 @@ import { } from '@kbn/kibana-utils-plugin/public'; import { connectToQueryState, DataPublicPluginStart, FilterManager } from '@kbn/data-plugin/public'; +import { DataView } from '@kbn/data-views-plugin/common'; +import { getValidFilters } from '../../../utils/get_valid_filters'; import { handleSourceColumnState } from '../../../utils/state_helpers'; export interface AppState { @@ -83,6 +85,11 @@ export interface GetStateParams { * data service */ data: DataPublicPluginStart; + + /** + * the current data view + */ + dataView: DataView; } export interface GetStateReturn { @@ -134,6 +141,7 @@ export function getState({ toasts, uiSettings, data, + dataView, }: GetStateParams): GetStateReturn { const stateStorage = createKbnUrlStateStorage({ useHash: storeInSessionStorage, @@ -191,7 +199,9 @@ export function getState({ globalState: globalStateContainer, appState: appStateContainer, startSync: () => { - data.query.filterManager.setFilters(cloneDeep(getAllFilters())); + // some filters may not be valid for this context, so update + // the filter manager with a modified list of valid filters + data.query.filterManager.setFilters(getValidFilters(dataView, getAllFilters())); const stopSyncingAppFilters = connectToQueryState(data.query, appStateContainer, { filters: FilterStateStore.APP_STATE, diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx index aae6e0430a7d8a..75bbedc0b8513e 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx @@ -16,8 +16,7 @@ import { esHits } from '../../../../__mocks__/es_hits'; import { dataViewMock } from '../../../../__mocks__/data_view'; import { savedSearchMock } from '../../../../__mocks__/saved_search'; import { createSearchSourceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; -import type { DataView, DataViewAttributes } from '@kbn/data-views-plugin/public'; -import { SavedObject } from '@kbn/core/types'; +import type { DataView } from '@kbn/data-views-plugin/public'; import { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield'; import { GetStateReturn } from '../../services/discover_state'; import { DiscoverLayoutProps } from './types'; @@ -59,9 +58,7 @@ function mountComponent( return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; }; - const dataViewList = [dataView].map((ip) => { - return { ...ip, ...{ attributes: { title: ip.title } } }; - }) as unknown as Array>; + const dataViewList = [dataView]; const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.COMPLETE, diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index 97c79647d32e18..a0262889187e70 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -24,7 +24,7 @@ import { isOfQueryType } from '@kbn/es-query'; import classNames from 'classnames'; import { generateFilters } from '@kbn/data-plugin/public'; import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/public'; -import { InspectorSession } from '@kbn/inspector-plugin/public'; +import { useInspector } from '../../hooks/use_inspector'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DiscoverNoResults } from '../no_results'; import { LoadingSpinner } from '../loading_spinner/loading_spinner'; @@ -89,7 +89,6 @@ export function DiscoverLayout({ inspector, } = useDiscoverServices(); const { main$, charts$, totalHits$ } = savedSearchData$; - const [inspectorSession, setInspectorSession] = useState(undefined); const dataState: DataMainMsg = useDataState(main$); const viewMode = useMemo(() => { @@ -141,23 +140,12 @@ export function DiscoverLayout({ [dataState.fetchStatus, dataState.foundDocuments, isPlainRecord] ); - const onOpenInspector = useCallback(() => { - // prevent overlapping - setExpandedDoc(undefined); - const session = inspector.open(inspectorAdapters, { - title: savedSearch.title, - }); - setInspectorSession(session); - }, [setExpandedDoc, inspectorAdapters, savedSearch, inspector]); - - useEffect(() => { - return () => { - if (inspectorSession) { - // Close the inspector if this scope is destroyed (e.g. because the user navigates away). - inspectorSession.close(); - } - }; - }, [inspectorSession]); + const onOpenInspector = useInspector({ + setExpandedDoc, + inspector, + inspectorAdapters, + savedSearch, + }); const { columns, onAddColumn, onRemoveColumn } = useColumns({ capabilities, diff --git a/src/plugins/discover/public/application/main/components/layout/types.ts b/src/plugins/discover/public/application/main/components/layout/types.ts index e254dd8774a37a..828b3bac4aa65d 100644 --- a/src/plugins/discover/public/application/main/components/layout/types.ts +++ b/src/plugins/discover/public/application/main/components/layout/types.ts @@ -7,9 +7,8 @@ */ import type { Query, TimeRange, AggregateQuery } from '@kbn/es-query'; -import type { SavedObject } from '@kbn/data-plugin/public'; -import type { DataView, DataViewAttributes } from '@kbn/data-views-plugin/public'; -import { ISearchSource } from '@kbn/data-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import { DataViewListItem, ISearchSource } from '@kbn/data-plugin/public'; import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { SavedSearch } from '@kbn/saved-search-plugin/public'; import { DataTableRecord } from '../../../../types'; @@ -18,7 +17,7 @@ import { DataRefetch$, SavedSearchData } from '../../hooks/use_saved_search'; export interface DiscoverLayoutProps { dataView: DataView; - dataViewList: Array>; + dataViewList: DataViewListItem[]; inspectorAdapters: { requests: RequestAdapter }; navigateTo: (url: string) => void; onChangeDataView: (id: string) => void; diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx index b5b612a9ed2762..c6a8eb20143ea6 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx @@ -12,8 +12,8 @@ import { getDataTableRecords } from '../../../../__fixtures__/real_hits'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; import { DiscoverSidebarProps } from './discover_sidebar'; -import { DataViewAttributes } from '@kbn/data-views-plugin/public'; -import { SavedObject } from '@kbn/core/types'; +import { DataViewListItem } from '@kbn/data-views-plugin/public'; + import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebarComponent as DiscoverSidebar } from './discover_sidebar'; import { discoverServiceMock as mockDiscoverServices } from '../../../../__mocks__/services'; @@ -39,9 +39,9 @@ function getCompProps(): DiscoverSidebarProps { const hits = getDataTableRecords(dataView); const dataViewList = [ - { id: '0', attributes: { title: 'b' } } as SavedObject, - { id: '1', attributes: { title: 'a' } } as SavedObject, - { id: '2', attributes: { title: 'c' } } as SavedObject, + { id: '0', title: 'b' } as DataViewListItem, + { id: '1', title: 'a' } as DataViewListItem, + { id: '2', title: 'c' } as DataViewListItem, ]; const fieldCounts: Record = {}; diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx index 05f1e6a960918f..1623a75a621d1e 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx @@ -13,8 +13,7 @@ import { getDataTableRecords } from '../../../../__fixtures__/real_hits'; import { act } from 'react-dom/test-utils'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; -import { DataViewAttributes } from '@kbn/data-views-plugin/public'; -import { SavedObject } from '@kbn/core/types'; +import { DataViewListItem } from '@kbn/data-views-plugin/public'; import { DiscoverSidebarResponsive, DiscoverSidebarResponsiveProps, @@ -78,9 +77,9 @@ function getCompProps(): DiscoverSidebarResponsiveProps { const hits = getDataTableRecords(dataView); const dataViewList = [ - { id: '0', attributes: { title: 'b' } } as SavedObject, - { id: '1', attributes: { title: 'a' } } as SavedObject, - { id: '2', attributes: { title: 'c' } } as SavedObject, + { id: '0', title: 'b' } as DataViewListItem, + { id: '1', title: 'a' } as DataViewListItem, + { id: '2', title: 'c' } as DataViewListItem, ]; for (const hit of hits) { diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx index bc813119342efc..d111c357223466 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx @@ -22,8 +22,7 @@ import { EuiShowFor, EuiTitle, } from '@elastic/eui'; -import type { DataView, DataViewAttributes, DataViewField } from '@kbn/data-views-plugin/public'; -import { SavedObject } from '@kbn/core/types'; +import type { DataView, DataViewField, DataViewListItem } from '@kbn/data-views-plugin/public'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebar } from './discover_sidebar'; @@ -51,7 +50,7 @@ export interface DiscoverSidebarResponsiveProps { /** * List of available data views */ - dataViewList: Array>; + dataViewList: DataViewListItem[]; /** * Has been toggled closed */ diff --git a/src/plugins/discover/public/application/main/discover_main_app.test.tsx b/src/plugins/discover/public/application/main/discover_main_app.test.tsx index bb7321ede1b8e7..0947891e5095f8 100644 --- a/src/plugins/discover/public/application/main/discover_main_app.test.tsx +++ b/src/plugins/discover/public/application/main/discover_main_app.test.tsx @@ -7,12 +7,11 @@ */ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { DataViewListItem } from '@kbn/data-views-plugin/public'; import { dataViewMock } from '../../__mocks__/data_view'; import { DiscoverMainApp } from './discover_main_app'; import { DiscoverTopNav } from './components/top_nav/discover_topnav'; import { savedSearchMock } from '../../__mocks__/saved_search'; -import { SavedObject } from '@kbn/core/types'; -import type { DataViewAttributes } from '@kbn/data-views-plugin/public'; import { setHeaderActionMenuMounter } from '../../kibana_services'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { discoverServiceMock } from '../../__mocks__/services'; @@ -25,7 +24,7 @@ describe('DiscoverMainApp', () => { test('renders', () => { const dataViewList = [dataViewMock].map((ip) => { return { ...ip, ...{ attributes: { title: ip.title } } }; - }) as unknown as Array>; + }) as unknown as DataViewListItem[]; const props = { dataViewList, savedSearch: savedSearchMock, diff --git a/src/plugins/discover/public/application/main/discover_main_app.tsx b/src/plugins/discover/public/application/main/discover_main_app.tsx index 9b48be3ad1bc81..4d129ae19746f1 100644 --- a/src/plugins/discover/public/application/main/discover_main_app.tsx +++ b/src/plugins/discover/public/application/main/discover_main_app.tsx @@ -7,9 +7,8 @@ */ import React, { useCallback, useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; -import type { DataViewAttributes } from '@kbn/data-views-plugin/public'; -import type { SavedObject } from '@kbn/data-plugin/public'; import { SavedSearch } from '@kbn/saved-search-plugin/public'; +import { DataViewListItem } from '@kbn/data-views-plugin/public'; import { DiscoverLayout } from './components/layout'; import { setBreadcrumbsTitle } from '../../utils/breadcrumbs'; import { addHelpMenuToAppChrome } from '../../components/help_menu/help_menu_util'; @@ -25,7 +24,7 @@ export interface DiscoverMainProps { /** * List of available data views */ - dataViewList: Array>; + dataViewList: DataViewListItem[]; /** * Current instance of SavedSearch */ @@ -35,7 +34,7 @@ export interface DiscoverMainProps { export function DiscoverMainApp(props: DiscoverMainProps) { const { savedSearch, dataViewList } = props; const services = useDiscoverServices(); - const { chrome, docLinks, uiSettings: config, data, spaces, history } = services; + const { chrome, docLinks, data, spaces, history } = services; const usedHistory = useHistory(); const [expandedDoc, setExpandedDoc] = useState(undefined); const navigateTo = useCallback( @@ -64,6 +63,7 @@ export function DiscoverMainApp(props: DiscoverMainProps) { history: usedHistory, savedSearch, setExpandedDoc, + dataViewList, }); /** @@ -81,7 +81,7 @@ export function DiscoverMainApp(props: DiscoverMainProps) { return () => { data.search.session.clear(); }; - }, [savedSearch, chrome, docLinks, refetch$, stateContainer, data, config]); + }, [savedSearch, chrome, data]); /** * Initializing syncing with state and help menu diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx index 06727473771b35..4871d498a9bdaa 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -7,12 +7,9 @@ */ import React, { useEffect, useState, memo, useCallback } from 'react'; import { useParams, useHistory } from 'react-router-dom'; -import { SavedObject } from '@kbn/data-plugin/public'; +import { DataViewListItem } from '@kbn/data-plugin/public'; import { ISearchSource } from '@kbn/data-plugin/public'; -import { - DataViewAttributes, - DataViewSavedObjectConflictError, -} from '@kbn/data-views-plugin/public'; +import { DataViewSavedObjectConflictError } from '@kbn/data-views-plugin/public'; import { redirectWhenMissing } from '@kbn/kibana-utils-plugin/public'; import { useExecutionContext } from '@kbn/kibana-react-plugin/public'; import { @@ -60,7 +57,7 @@ export function DiscoverMainRoute(props: Props) { const [error, setError] = useState(); const [savedSearch, setSavedSearch] = useState(); const dataView = savedSearch?.searchSource?.getField('index'); - const [dataViewList, setDataViewList] = useState>>([]); + const [dataViewList, setDataViewList] = useState([]); const [hasESData, setHasESData] = useState(false); const [hasUserDataView, setHasUserDataView] = useState(false); const [showNoDataPage, setShowNoDataPage] = useState(false); @@ -99,7 +96,7 @@ export function DiscoverMainRoute(props: Props) { const { index } = appStateContainer.getState(); const ip = await loadDataView(index || '', data.dataViews, config); - const ipList = ip.list as Array>; + const ipList = ip.list; const dataViewData = resolveDataView(ip, searchSource, toastNotifications); await data.dataViews.refreshFields(dataViewData); setDataViewList(ipList); diff --git a/src/plugins/discover/public/application/main/hooks/use_discover_state.test.ts b/src/plugins/discover/public/application/main/hooks/use_discover_state.test.ts index c7b6c7a6fe8e7a..40078a4616e8e1 100644 --- a/src/plugins/discover/public/application/main/hooks/use_discover_state.test.ts +++ b/src/plugins/discover/public/application/main/hooks/use_discover_state.test.ts @@ -7,28 +7,14 @@ */ import { renderHook } from '@testing-library/react-hooks'; +import { DataViewListItem, SearchSource } from '@kbn/data-plugin/public'; import { createSearchSessionMock } from '../../../__mocks__/search_session'; import { discoverServiceMock } from '../../../__mocks__/services'; import { savedSearchMock } from '../../../__mocks__/saved_search'; import { useDiscoverState } from './use_discover_state'; import { dataViewMock } from '../../../__mocks__/data_view'; -import { SearchSource } from '@kbn/data-plugin/public'; describe('test useDiscoverState', () => { - const originalSavedObjectsClient = discoverServiceMock.core.savedObjects.client; - - beforeAll(() => { - discoverServiceMock.core.savedObjects.client.resolve = jest.fn().mockReturnValue({ - saved_object: { - attributes: {}, - }, - }); - }); - - afterAll(() => { - discoverServiceMock.core.savedObjects.client = originalSavedObjectsClient; - }); - test('return is valid', async () => { const { history } = createSearchSessionMock(); @@ -38,6 +24,7 @@ describe('test useDiscoverState', () => { history, savedSearch: savedSearchMock, setExpandedDoc: jest.fn(), + dataViewList: [dataViewMock as DataViewListItem], }); }); expect(result.current.state.index).toBe(dataViewMock.id); diff --git a/src/plugins/discover/public/application/main/hooks/use_discover_state.ts b/src/plugins/discover/public/application/main/hooks/use_discover_state.ts index dc06163a5f2c1e..c89d8b206ecf1c 100644 --- a/src/plugins/discover/public/application/main/hooks/use_discover_state.ts +++ b/src/plugins/discover/public/application/main/hooks/use_discover_state.ts @@ -6,23 +6,17 @@ * Side Public License, v 1. */ import { useMemo, useEffect, useState, useCallback } from 'react'; -import usePrevious from 'react-use/lib/usePrevious'; import { isEqual } from 'lodash'; import { History } from 'history'; -import { DataViewType } from '@kbn/data-views-plugin/public'; -import { - isOfAggregateQueryType, - getIndexPatternFromSQLQuery, - AggregateQuery, - Query, -} from '@kbn/es-query'; +import { DataViewListItem, DataViewType } from '@kbn/data-views-plugin/public'; import { SavedSearch, getSavedSearch } from '@kbn/saved-search-plugin/public'; import type { SortOrder } from '@kbn/saved-search-plugin/public'; +import { useTextBasedQueryLanguage } from './use_text_based_query_language'; import { getState } from '../services/discover_state'; import { getStateDefaults } from '../utils/get_state_defaults'; import { DiscoverServices } from '../../../build_services'; import { loadDataView } from '../utils/resolve_data_view'; -import { useSavedSearch as useSavedSearchData, DataDocumentsMsg } from './use_saved_search'; +import { useSavedSearch as useSavedSearchData } from './use_saved_search'; import { MODIFY_COLUMNS_ON_SWITCH, SEARCH_FIELDS_FROM_SOURCE, @@ -30,27 +24,26 @@ import { SORT_DEFAULT_ORDER_SETTING, } from '../../../../common'; import { useSearchSession } from './use_search_session'; -import { useDataState } from './use_data_state'; import { FetchStatus } from '../../types'; import { getDataViewAppState } from '../utils/get_switch_data_view_app_state'; import { DataTableRecord } from '../../../types'; import { restoreStateFromSavedSearch } from '../../../services/saved_searches/restore_from_saved_search'; -const MAX_NUM_OF_COLUMNS = 50; - export function useDiscoverState({ services, history, savedSearch, setExpandedDoc, + dataViewList, }: { services: DiscoverServices; savedSearch: SavedSearch; history: History; setExpandedDoc: (doc?: DataTableRecord) => void; + dataViewList: DataViewListItem[]; }) { - const { uiSettings: config, data, filterManager, dataViews, storage } = services; - const useNewFieldsApi = useMemo(() => !config.get(SEARCH_FIELDS_FROM_SOURCE), [config]); + const { uiSettings, data, filterManager, dataViews, storage } = services; + const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]); const { timefilter } = data.query.timefilter; const dataView = savedSearch.searchSource.getField('index')!; @@ -65,25 +58,22 @@ export function useDiscoverState({ getState({ getStateDefaults: () => getStateDefaults({ - config, + config: uiSettings, data, savedSearch, storage, }), - storeInSessionStorage: config.get('state:storeInSessionStorage'), + storeInSessionStorage: uiSettings.get('state:storeInSessionStorage'), history, toasts: services.core.notifications.toasts, - uiSettings: config, + uiSettings, }), - [config, data, history, savedSearch, services.core.notifications.toasts, storage] + [uiSettings, data, history, savedSearch, services.core.notifications.toasts, storage] ); const { appStateContainer } = stateContainer; const [state, setState] = useState(appStateContainer.getState()); - const [documentStateCols, setDocumentStateCols] = useState([]); - const [sqlQuery] = useState(state.query); - const prevQuery = usePrevious(state.query); /** * Search session logic @@ -94,12 +84,12 @@ export function useDiscoverState({ // A saved search is created on every page load, so we check the ID to see if we're loading a // previously saved search or if it is just transient const shouldSearchOnPageLoad = - config.get(SEARCH_ON_PAGE_LOAD_SETTING) || + uiSettings.get(SEARCH_ON_PAGE_LOAD_SETTING) || savedSearch.id !== undefined || timefilter.getRefreshInterval().pause === false || searchSessionManager.hasSearchSessionIdInURL(); return shouldSearchOnPageLoad ? FetchStatus.LOADING : FetchStatus.UNINITIALIZED; - }, [config, savedSearch.id, searchSessionManager, timefilter]); + }, [uiSettings, savedSearch.id, searchSessionManager, timefilter]); /** * Data fetching logic @@ -113,8 +103,16 @@ export function useDiscoverState({ stateContainer, useNewFieldsApi, }); - - const documentState: DataDocumentsMsg = useDataState(data$.documents$); + /** + * State changes (data view, columns), when a text base query result is returned + */ + useTextBasedQueryLanguage({ + documents$: data$.documents$, + dataViews, + stateContainer, + dataViewList, + savedSearch, + }); /** * Reset to display loading spinner when savedSearch is changing @@ -151,7 +149,11 @@ export function useDiscoverState({ * That's because appState is updated before savedSearchData$ * The following line of code catches this, but should be improved */ - const nextDataView = await loadDataView(nextState.index, dataViews, config); + const nextDataView = await loadDataView( + nextState.index, + services.dataViews, + services.uiSettings + ); savedSearch.searchSource.setField('index', nextDataView.loaded); reset(); @@ -163,17 +165,7 @@ export function useDiscoverState({ setState(nextState); }); return () => unsubscribe(); - }, [ - config, - dataViews, - appStateContainer, - setState, - state, - refetch$, - data$, - reset, - savedSearch.searchSource, - ]); + }, [services, appStateContainer, state, refetch$, data$, reset, savedSearch.searchSource]); /** * function to revert any changes to a given saved search @@ -190,7 +182,7 @@ export function useDiscoverState({ const newDataView = newSavedSearch.searchSource.getField('index') || dataView; newSavedSearch.searchSource.setField('index', newDataView); const newAppState = getStateDefaults({ - config, + config: uiSettings, data, savedSearch: newSavedSearch, storage, @@ -204,7 +196,7 @@ export function useDiscoverState({ await stateContainer.replaceUrlAppState(newAppState); setState(newAppState); }, - [services, dataView, config, data, storage, stateContainer] + [services, dataView, uiSettings, data, storage, stateContainer] ); /** @@ -219,8 +211,8 @@ export function useDiscoverState({ nextDataView, state.columns || [], (state.sort || []) as SortOrder[], - config.get(MODIFY_COLUMNS_ON_SWITCH), - config.get(SORT_DEFAULT_ORDER_SETTING), + uiSettings.get(MODIFY_COLUMNS_ON_SWITCH), + uiSettings.get(SORT_DEFAULT_ORDER_SETTING), state.query ); stateContainer.setAppState(nextAppState); @@ -228,7 +220,7 @@ export function useDiscoverState({ setExpandedDoc(undefined); }, [ - config, + uiSettings, dataView, dataViews, setExpandedDoc, @@ -254,12 +246,6 @@ export function useDiscoverState({ /** * Trigger data fetching on dataView or savedSearch changes */ - useEffect(() => { - if (!isEqual(state.query, prevQuery)) { - setDocumentStateCols([]); - } - }, [state.query, prevQuery]); - useEffect(() => { if (dataView) { refetch$.next(undefined); @@ -276,41 +262,6 @@ export function useDiscoverState({ } }, [dataView, stateContainer]); - const getResultColumns = useCallback(() => { - if (documentState.result?.length && documentState.fetchStatus === FetchStatus.COMPLETE) { - const firstRow = documentState.result[0]; - const columns = Object.keys(firstRow.raw).slice(0, MAX_NUM_OF_COLUMNS); - if (!isEqual(columns, documentStateCols) && !isEqual(state.query, sqlQuery)) { - return columns; - } - return []; - } - return []; - }, [documentState, documentStateCols, sqlQuery, state.query]); - - useEffect(() => { - async function fetchDataview() { - if (state.query && isOfAggregateQueryType(state.query) && 'sql' in state.query) { - const indexPatternFromQuery = getIndexPatternFromSQLQuery(state.query.sql); - const idsTitles = await dataViews.getIdsWithTitle(); - const dataViewObj = idsTitles.find(({ title }) => title === indexPatternFromQuery); - if (dataViewObj) { - const columns = getResultColumns(); - if (columns.length) { - setDocumentStateCols(columns); - } - const nextState = { - index: dataViewObj.id, - ...(columns.length && { columns }), - }; - stateContainer.replaceUrlAppState(nextState); - } - } - } - fetchDataview(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [config, documentState, dataViews]); - return { data$, dataView, diff --git a/src/plugins/discover/public/application/main/hooks/use_inspector.test.ts b/src/plugins/discover/public/application/main/hooks/use_inspector.test.ts new file mode 100644 index 00000000000000..66c5542f5647a6 --- /dev/null +++ b/src/plugins/discover/public/application/main/hooks/use_inspector.test.ts @@ -0,0 +1,31 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { discoverServiceMock } from '../../../__mocks__/services'; +import { savedSearchMock } from '../../../__mocks__/saved_search'; +import { useInspector } from './use_inspector'; +import { RequestAdapter } from '@kbn/inspector-plugin/common'; + +describe('test useInspector', () => { + test('inspector open function is executed, expanded doc is closed', async () => { + const setExpandedDoc = jest.fn(); + + const { result } = renderHook(() => { + return useInspector({ + inspectorAdapters: { requests: new RequestAdapter() }, + savedSearch: savedSearchMock, + inspector: discoverServiceMock.inspector, + setExpandedDoc, + }); + }); + result.current(); + expect(setExpandedDoc).toHaveBeenCalledWith(undefined); + expect(discoverServiceMock.inspector.open).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover/public/application/main/hooks/use_inspector.ts b/src/plugins/discover/public/application/main/hooks/use_inspector.ts new file mode 100644 index 00000000000000..c7bcc0ba1cb4b7 --- /dev/null +++ b/src/plugins/discover/public/application/main/hooks/use_inspector.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ + +import { useCallback, useEffect, useState } from 'react'; +import { + InspectorSession, + RequestAdapter, + Start as InspectorPublicPluginStart, +} from '@kbn/inspector-plugin/public'; +import { SavedSearch } from '@kbn/saved-search-plugin/public'; +import { DataTableRecord } from '../../../types'; + +export function useInspector({ + setExpandedDoc, + inspector, + inspectorAdapters, + savedSearch, +}: { + inspectorAdapters: { requests: RequestAdapter }; + savedSearch: SavedSearch; + setExpandedDoc: (doc?: DataTableRecord) => void; + inspector: InspectorPublicPluginStart; +}) { + const [inspectorSession, setInspectorSession] = useState(undefined); + + const onOpenInspector = useCallback(() => { + // prevent overlapping + setExpandedDoc(undefined); + const session = inspector.open(inspectorAdapters, { + title: savedSearch.title, + }); + setInspectorSession(session); + }, [setExpandedDoc, inspectorAdapters, savedSearch, inspector]); + + useEffect(() => { + return () => { + if (inspectorSession) { + // Close the inspector if this scope is destroyed (e.g. because the user navigates away). + inspectorSession.close(); + } + }; + }, [inspectorSession]); + return onOpenInspector; +} diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search.test.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search.test.ts index 1f3dfdbf39771f..41b8348a3a26df 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search.test.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search.test.ts @@ -15,6 +15,8 @@ import { getState, AppState } from '../services/discover_state'; import { uiSettingsMock } from '../../../__mocks__/ui_settings'; import { useDiscoverState } from './use_discover_state'; import { FetchStatus } from '../../types'; +import { dataViewMock } from '../../../__mocks__/data_view'; +import { DataViewListItem } from '@kbn/data-views-plugin/common'; describe('test useSavedSearch', () => { test('useSavedSearch return is valid', async () => { @@ -61,6 +63,7 @@ describe('test useSavedSearch', () => { history, savedSearch: savedSearchMock, setExpandedDoc: jest.fn(), + dataViewList: [dataViewMock as DataViewListItem], }); }); @@ -104,6 +107,7 @@ describe('test useSavedSearch', () => { history, savedSearch: savedSearchMock, setExpandedDoc: jest.fn(), + dataViewList: [dataViewMock as DataViewListItem], }); }); diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search.ts index 3b9948f88d17e3..4762748e2618b7 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search.ts @@ -11,6 +11,7 @@ import type { AutoRefreshDoneFn } from '@kbn/data-plugin/public'; import { ISearchSource } from '@kbn/data-plugin/public'; import { RequestAdapter } from '@kbn/inspector-plugin/public'; import { SavedSearch } from '@kbn/saved-search-plugin/public'; +import { AggregateQuery, Query } from '@kbn/es-query'; import { getRawRecordType } from '../utils/get_raw_record_type'; import { DiscoverServices } from '../../../build_services'; import { DiscoverSearchSessionManager } from '../services/discover_search_session'; @@ -71,6 +72,7 @@ export interface DataMsg { fetchStatus: FetchStatus; error?: Error; recordRawType?: RecordRawType; + query?: AggregateQuery | Query | undefined; } export interface DataMainMsg extends DataMsg { diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts index a17f33b5270c1c..0d6caf180a3f61 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { AggregateQuery, Query } from '@kbn/es-query'; import { FetchStatus } from '../../types'; import { DataCharts$, @@ -61,12 +62,14 @@ export function sendPartialMsg(main$: DataMain$) { */ export function sendLoadingMsg( data$: DataMain$ | DataDocuments$ | DataTotalHits$ | DataCharts$, - recordRawType: RecordRawType + recordRawType: RecordRawType, + query?: AggregateQuery | Query ) { if (data$.getValue().fetchStatus !== FetchStatus.LOADING) { data$.next({ fetchStatus: FetchStatus.LOADING, recordRawType, + query, }); } } diff --git a/src/plugins/discover/public/application/main/hooks/use_test_based_query_language.test.ts b/src/plugins/discover/public/application/main/hooks/use_test_based_query_language.test.ts new file mode 100644 index 00000000000000..4efbfe827c4bda --- /dev/null +++ b/src/plugins/discover/public/application/main/hooks/use_test_based_query_language.test.ts @@ -0,0 +1,249 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { waitFor } from '@testing-library/react'; +import { discoverServiceMock } from '../../../__mocks__/services'; +import { useTextBasedQueryLanguage } from './use_text_based_query_language'; +import { AppState, GetStateReturn } from '../services/discover_state'; +import { BehaviorSubject } from 'rxjs'; +import { FetchStatus } from '../../types'; +import { DataDocuments$, RecordRawType } from './use_saved_search'; +import { DataTableRecord } from '../../../types'; +import { AggregateQuery, Query } from '@kbn/es-query'; +import { dataViewMock } from '../../../__mocks__/data_view'; +import { DataViewListItem } from '@kbn/data-views-plugin/common'; +import { savedSearchMock } from '../../../__mocks__/saved_search'; + +function getHookProps( + replaceUrlAppState: (newState: Partial) => Promise, + query: AggregateQuery | Query | undefined +) { + const stateContainer = { + replaceUrlAppState, + appStateContainer: { + getState: () => { + return []; + }, + }, + } as unknown as GetStateReturn; + + const msgLoading = { + recordRawType: RecordRawType.PLAIN, + fetchStatus: FetchStatus.LOADING, + query, + }; + + const documents$ = new BehaviorSubject(msgLoading) as DataDocuments$; + + return { + documents$, + dataViews: discoverServiceMock.dataViews, + stateContainer, + dataViewList: [dataViewMock as DataViewListItem], + savedSearch: savedSearchMock, + }; +} +const query = { sql: 'SELECT * from the-data-view-title' }; +const msgComplete = { + recordRawType: RecordRawType.PLAIN, + fetchStatus: FetchStatus.COMPLETE, + result: [ + { + id: '1', + raw: { field1: 1, field2: 2 }, + flattened: { field1: 1, field2: 2 }, + } as unknown as DataTableRecord, + ], + query, +}; + +describe('useTextBasedQueryLanguage', () => { + test('a text based query should change state when loading and finished', async () => { + const replaceUrlAppState = jest.fn(); + const props = getHookProps(replaceUrlAppState, query); + const { documents$ } = props; + + renderHook(() => useTextBasedQueryLanguage(props)); + + await waitFor(() => expect(replaceUrlAppState).toHaveBeenCalledTimes(1)); + expect(replaceUrlAppState).toHaveBeenCalledWith({ index: 'the-data-view-id' }); + + replaceUrlAppState.mockReset(); + + documents$.next(msgComplete); + await waitFor(() => expect(replaceUrlAppState).toHaveBeenCalledTimes(1)); + + await waitFor(() => { + expect(replaceUrlAppState).toHaveBeenCalledWith({ + index: 'the-data-view-id', + columns: ['field1', 'field2'], + }); + }); + }); + test('changing a text based query with different result columns should change state when loading and finished', async () => { + const replaceUrlAppState = jest.fn(); + const props = getHookProps(replaceUrlAppState, query); + const { documents$ } = props; + + renderHook(() => useTextBasedQueryLanguage(props)); + + documents$.next(msgComplete); + await waitFor(() => expect(replaceUrlAppState).toHaveBeenCalledTimes(2)); + replaceUrlAppState.mockReset(); + + documents$.next({ + recordRawType: RecordRawType.PLAIN, + fetchStatus: FetchStatus.COMPLETE, + result: [ + { + id: '1', + raw: { field1: 1 }, + flattened: { field1: 1 }, + } as unknown as DataTableRecord, + ], + query: { sql: 'SELECT field1 from the-data-view-title' }, + }); + await waitFor(() => expect(replaceUrlAppState).toHaveBeenCalledTimes(1)); + + await waitFor(() => { + expect(replaceUrlAppState).toHaveBeenCalledWith({ + index: 'the-data-view-id', + columns: ['field1'], + }); + }); + }); + test('only changing a text based query with same result columns should not change columns', async () => { + const replaceUrlAppState = jest.fn(); + const props = getHookProps(replaceUrlAppState, query); + const { documents$ } = props; + + renderHook(() => useTextBasedQueryLanguage(props)); + + documents$.next(msgComplete); + await waitFor(() => expect(replaceUrlAppState).toHaveBeenCalledTimes(2)); + replaceUrlAppState.mockReset(); + + documents$.next({ + recordRawType: RecordRawType.PLAIN, + fetchStatus: FetchStatus.COMPLETE, + result: [ + { + id: '1', + raw: { field1: 1 }, + flattened: { field1: 1 }, + } as unknown as DataTableRecord, + ], + query: { sql: 'SELECT field1 from the-data-view-title' }, + }); + await waitFor(() => expect(replaceUrlAppState).toHaveBeenCalledTimes(1)); + replaceUrlAppState.mockReset(); + + documents$.next({ + recordRawType: RecordRawType.PLAIN, + fetchStatus: FetchStatus.COMPLETE, + result: [ + { + id: '1', + raw: { field1: 1 }, + flattened: { field1: 1 }, + } as unknown as DataTableRecord, + ], + query: { sql: 'SELECT field1 from the-data-view-title WHERE field1=1' }, + }); + + await waitFor(() => { + expect(replaceUrlAppState).toHaveBeenCalledWith({ + index: 'the-data-view-id', + }); + }); + }); + test('if its not a text based query coming along, it should be ignored', async () => { + const replaceUrlAppState = jest.fn(); + const props = getHookProps(replaceUrlAppState, query); + const { documents$ } = props; + + renderHook(() => useTextBasedQueryLanguage(props)); + + documents$.next(msgComplete); + await waitFor(() => expect(replaceUrlAppState).toHaveBeenCalledTimes(2)); + replaceUrlAppState.mockReset(); + + documents$.next({ + recordRawType: RecordRawType.DOCUMENT, + fetchStatus: FetchStatus.COMPLETE, + result: [ + { + id: '1', + raw: { field1: 1 }, + flattened: { field1: 1 }, + } as unknown as DataTableRecord, + ], + }); + + documents$.next({ + recordRawType: RecordRawType.PLAIN, + fetchStatus: FetchStatus.COMPLETE, + result: [ + { + id: '1', + raw: { field1: 1 }, + flattened: { field1: 1 }, + } as unknown as DataTableRecord, + ], + query: { sql: 'SELECT field1 from the-data-view-title WHERE field1=1' }, + }); + + await waitFor(() => { + expect(replaceUrlAppState).toHaveBeenCalledWith({ + index: 'the-data-view-id', + columns: ['field1'], + }); + }); + }); + + test('it should not overwrite existing state columns on initial fetch', async () => { + const replaceUrlAppState = jest.fn(); + const props = getHookProps(replaceUrlAppState, query); + props.stateContainer.appStateContainer.getState = jest.fn(() => { + return { columns: ['field1'], index: 'the-data-view-id' }; + }); + const { documents$ } = props; + + renderHook(() => useTextBasedQueryLanguage(props)); + documents$.next({ + recordRawType: RecordRawType.PLAIN, + fetchStatus: FetchStatus.COMPLETE, + result: [ + { + id: '1', + raw: { field1: 1, field2: 2 }, + flattened: { field1: 1 }, + } as unknown as DataTableRecord, + ], + query: { sql: 'SELECT field1 from the-data-view-title WHERE field1=1' }, + }); + + documents$.next({ + recordRawType: RecordRawType.PLAIN, + fetchStatus: FetchStatus.COMPLETE, + result: [ + { + id: '1', + raw: { field1: 1 }, + flattened: { field1: 1 }, + } as unknown as DataTableRecord, + ], + query: { sql: 'SELECT field1 from the-data-view-title' }, + }); + await waitFor(() => expect(replaceUrlAppState).toHaveBeenCalledTimes(1)); + expect(replaceUrlAppState).toHaveBeenCalledWith({ + columns: ['field1'], + }); + }); +}); diff --git a/src/plugins/discover/public/application/main/hooks/use_text_based_query_language.ts b/src/plugins/discover/public/application/main/hooks/use_text_based_query_language.ts new file mode 100644 index 00000000000000..71732bfaf6f27f --- /dev/null +++ b/src/plugins/discover/public/application/main/hooks/use_text_based_query_language.ts @@ -0,0 +1,113 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ +import { isEqual } from 'lodash'; +import { + isOfAggregateQueryType, + getIndexPatternFromSQLQuery, + AggregateQuery, + Query, +} from '@kbn/es-query'; +import { useCallback, useEffect, useRef } from 'react'; +import { DataViewListItem, DataViewsContract } from '@kbn/data-views-plugin/public'; +import { SavedSearch } from '@kbn/saved-search-plugin/public'; +import type { GetStateReturn } from '../services/discover_state'; +import type { DataDocuments$ } from './use_saved_search'; +import { FetchStatus } from '../../types'; + +const MAX_NUM_OF_COLUMNS = 50; + +/** + * Hook to take care of text based query language state transformations when a new result is returned + * If necessary this is setting displayed columns and selected data view + */ +export function useTextBasedQueryLanguage({ + documents$, + dataViews, + stateContainer, + dataViewList, + savedSearch, +}: { + documents$: DataDocuments$; + stateContainer: GetStateReturn; + dataViews: DataViewsContract; + dataViewList: DataViewListItem[]; + savedSearch: SavedSearch; +}) { + const prev = useRef<{ query: AggregateQuery | Query | undefined; columns: string[] }>({ + columns: [], + query: undefined, + }); + + const cleanup = useCallback(() => { + if (prev.current.query) { + // cleanup when it's not a text based query lang + prev.current = { + columns: [], + query: undefined, + }; + } + }, []); + + useEffect(() => { + const subscription = documents$.subscribe(async (next) => { + const { query } = next; + const { columns: stateColumns, index } = stateContainer.appStateContainer.getState(); + let nextColumns: string[] = []; + const isTextBasedQueryLang = + next.recordRawType === 'plain' && query && isOfAggregateQueryType(query) && 'sql' in query; + const hasResults = next.result?.length && next.fetchStatus === FetchStatus.COMPLETE; + const initialFetch = !prev.current.columns.length; + + if (isTextBasedQueryLang) { + if (hasResults) { + // check if state needs to contain column transformation due to a different columns in the resultset + const firstRow = next.result![0]; + const firstRowColumns = Object.keys(firstRow.raw).slice(0, MAX_NUM_OF_COLUMNS); + if ( + !isEqual(firstRowColumns, prev.current.columns) && + !isEqual(query, prev.current.query) + ) { + nextColumns = firstRowColumns; + prev.current = { columns: nextColumns, query }; + } + if (firstRowColumns && initialFetch) { + prev.current = { columns: firstRowColumns, query }; + } + } + const indexPatternFromQuery = getIndexPatternFromSQLQuery(query.sql); + const dataViewObj = dataViewList.find(({ title }) => title === indexPatternFromQuery); + + if (dataViewObj) { + // don't set the columns on initial fetch, to prevent overwriting existing state + const addColumnsToState = Boolean( + nextColumns.length && (!initialFetch || !stateColumns?.length) + ); + // no need to reset index to state if it hasn't changed + const addDataViewToState = Boolean(dataViewObj.id !== index); + if (!addColumnsToState && !addDataViewToState) { + return; + } + + const nextState = { + ...(addDataViewToState && { index: dataViewObj.id }), + ...(addColumnsToState && { columns: nextColumns }), + }; + stateContainer.replaceUrlAppState(nextState); + } + } else { + // cleanup for a "regular" query + cleanup(); + } + }); + return () => { + // cleanup for e.g. when savedSearch is switched + cleanup(); + subscription.unsubscribe(); + }; + }, [documents$, dataViews, stateContainer, dataViewList, savedSearch, cleanup]); +} diff --git a/src/plugins/discover/public/application/main/services/discover_state.ts b/src/plugins/discover/public/application/main/services/discover_state.ts index 33393eeecdd758..568a219b952e77 100644 --- a/src/plugins/discover/public/application/main/services/discover_state.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.ts @@ -42,6 +42,7 @@ import { handleSourceColumnState } from '../../../utils/state_helpers'; import { DISCOVER_APP_LOCATOR, DiscoverAppLocatorParams } from '../../../locator'; import { VIEW_MODE } from '../../../components/view_mode_toggle'; import { cleanupUrlState } from '../utils/cleanup_url_state'; +import { getValidFilters } from '../../../utils/get_valid_filters'; export interface AppState { /** @@ -319,6 +320,14 @@ export function getState({ stateStorage ); + // some filters may not be valid for this context, so update + // the filter manager with a modified list of valid filters + const currentFilters = filterManager.getFilters(); + const validFilters = getValidFilters(dataView, currentFilters); + if (!isEqual(currentFilters, validFilters)) { + filterManager.setFilters(validFilters); + } + const { start, stop } = syncAppState(); replaceUrlAppState({}).then(() => { diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts index 47566ed1095d3f..288595fa5f7a5d 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts @@ -242,10 +242,11 @@ describe('test fetchAll', () => { ]; const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock)); mockFetchSQL.mockResolvedValue(documents); + const query = { sql: 'SELECT * from foo' }; deps = { appStateContainer: { getState: () => { - return { interval: 'auto', query: { sql: 'SELECT * from foo' } }; + return { interval: 'auto', query }; }, } as unknown as ReduxLikeStateContainer, abortController: new AbortController(), @@ -260,11 +261,12 @@ describe('test fetchAll', () => { await fetchAll(subjects, searchSource, false, deps); expect(await collect()).toEqual([ { fetchStatus: FetchStatus.UNINITIALIZED }, - { fetchStatus: FetchStatus.LOADING, recordRawType: 'plain' }, + { fetchStatus: FetchStatus.LOADING, recordRawType: 'plain', query }, { fetchStatus: FetchStatus.COMPLETE, recordRawType: 'plain', result: documents, + query, }, ]); }); diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.ts b/src/plugins/discover/public/application/main/utils/fetch_all.ts index 42b415630e92c5..853ae6cebfb76a 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.ts @@ -65,7 +65,7 @@ export function fetchAll( const { initialFetchStatus, appStateContainer, services, useNewFieldsApi, data } = fetchDeps; /** - * Method to create a an error handler that will forward the received error + * Method to create an error handler that will forward the received error * to the specified subjects. It will ignore AbortErrors and will use the data * plugin to show a toast for the error (e.g. allowing better insights into shard failures). */ @@ -103,7 +103,7 @@ export function fetchAll( // Mark all subjects as loading sendLoadingMsg(dataSubjects.main$, recordRawType); - sendLoadingMsg(dataSubjects.documents$, recordRawType); + sendLoadingMsg(dataSubjects.documents$, recordRawType, query); sendLoadingMsg(dataSubjects.totalHits$, recordRawType); sendLoadingMsg(dataSubjects.charts$, recordRawType); @@ -152,6 +152,7 @@ export function fetchAll( fetchStatus: FetchStatus.COMPLETE, result: docs, recordRawType, + query, }); checkHitCount(docs.length); diff --git a/src/plugins/discover/public/application/main/utils/resolve_data_view.test.ts b/src/plugins/discover/public/application/main/utils/resolve_data_view.test.ts index 9db54b596f8354..2b5f0a4a9563a4 100644 --- a/src/plugins/discover/public/application/main/utils/resolve_data_view.test.ts +++ b/src/plugins/discover/public/application/main/utils/resolve_data_view.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { loadDataView, getFallbackDataViewId, DataViewSavedObject } from './resolve_data_view'; +import { loadDataView, getFallbackDataViewId } from './resolve_data_view'; import { dataViewsMock } from '../../../__mocks__/data_views'; import { dataViewMock } from '../../../__mocks__/data_view'; import { configMock } from '../../../__mocks__/config'; @@ -31,8 +31,8 @@ describe('Resolve data view tests', () => { expect(result).toBe(''); }); test('getFallbackDataViewId with an dataViews array', async () => { - const list = await dataViewsMock.getCache(); - const result = await getFallbackDataViewId(list as unknown as DataViewSavedObject[], ''); + const list = await dataViewsMock.getIdsWithTitle(); + const result = await getFallbackDataViewId(list, ''); expect(result).toBe('the-data-view-id'); }); }); diff --git a/src/plugins/discover/public/application/main/utils/resolve_data_view.ts b/src/plugins/discover/public/application/main/utils/resolve_data_view.ts index 3745d6130c01f3..b56abfcf316ae8 100644 --- a/src/plugins/discover/public/application/main/utils/resolve_data_view.ts +++ b/src/plugins/discover/public/application/main/utils/resolve_data_view.ts @@ -7,16 +7,14 @@ */ import { i18n } from '@kbn/i18n'; -import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public'; +import type { DataView, DataViewListItem, DataViewsContract } from '@kbn/data-views-plugin/public'; import type { ISearchSource } from '@kbn/data-plugin/public'; -import type { IUiSettingsClient, SavedObject, ToastsStart } from '@kbn/core/public'; -export type DataViewSavedObject = SavedObject & { title: string }; - +import type { IUiSettingsClient, ToastsStart } from '@kbn/core/public'; interface DataViewData { /** * List of existing data views */ - list: DataViewSavedObject[]; + list: DataViewListItem[]; /** * Loaded data view (might be default data view if requested was not found) */ @@ -32,9 +30,9 @@ interface DataViewData { } export function findDataViewById( - dataViews: DataViewSavedObject[], + dataViews: DataViewListItem[], id: string -): DataViewSavedObject | undefined { +): DataViewListItem | undefined { if (!Array.isArray(dataViews) || !id) { return; } @@ -46,7 +44,7 @@ export function findDataViewById( * the first available data view id if not */ export function getFallbackDataViewId( - dataViews: DataViewSavedObject[], + dataViews: DataViewListItem[], defaultIndex: string = '' ): string { if (defaultIndex && findDataViewById(dataViews, defaultIndex)) { @@ -62,7 +60,7 @@ export function getFallbackDataViewId( */ export function getDataViewId( id: string = '', - dataViews: DataViewSavedObject[] = [], + dataViews: DataViewListItem[] = [], defaultIndex: string = '' ): string { if (!id || !findDataViewById(dataViews, id)) { @@ -79,7 +77,7 @@ export async function loadDataView( dataViews: DataViewsContract, config: IUiSettingsClient ): Promise { - const dataViewList = (await dataViews.getCache()) as unknown as DataViewSavedObject[]; + const dataViewList = await dataViews.getIdsWithTitle(); const actualId = getDataViewId(id, dataViewList, config.get('defaultIndex')); return { diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx index f76343156c9554..00cbd0a2ffcb0f 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx @@ -484,6 +484,8 @@ export class SavedSearchEmbeddable ReactDOM.unmountComponentAtNode(this.node); } this.node = domNode; + + this.renderReactComponent(this.node, this.searchProps!); } private renderReactComponent(domNode: HTMLElement, searchProps: SearchProps) { diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_cell_actions.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_cell_actions.tsx index d67e12cf8eccc1..9f29f3ba7f69f0 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_cell_actions.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_cell_actions.tsx @@ -177,15 +177,13 @@ export const TableActions = ({ }, ]; - const testSubject = `openFieldActionsButton-${field}`; - if (mode === 'inline') { return ( {panels[0].items.map((item) => ( @@ -210,7 +208,7 @@ export const TableActions = ({ { + const filter = (index: string, disabled: boolean, script: boolean) => ({ + meta: { + index, + disabled, + }, + ...(script ? { query: { script: {} } } : {}), + }); + const dataView = { + id: '123', + } as DataView; + + it("should only disable scripted fields that don't match the current data view", () => { + const filters = getValidFilters(dataView, [ + filter('123', false, false), + filter('123', true, false), + filter('123', false, true), + filter('123', true, true), + filter('321', false, false), + filter('321', true, false), + filter('321', false, true), + filter('321', true, true), + ]); + expect(filters.length).toBe(8); + expect(filters[0].meta.disabled).toBe(false); + expect(filters[1].meta.disabled).toBe(true); + expect(filters[2].meta.disabled).toBe(false); + expect(filters[3].meta.disabled).toBe(true); + expect(filters[4].meta.disabled).toBe(false); + expect(filters[5].meta.disabled).toBe(true); + expect(filters[6].meta.disabled).toBe(true); + expect(filters[7].meta.disabled).toBe(true); + }); +}); diff --git a/src/plugins/discover/public/utils/get_valid_filters.ts b/src/plugins/discover/public/utils/get_valid_filters.ts new file mode 100644 index 00000000000000..2d43262efc1968 --- /dev/null +++ b/src/plugins/discover/public/utils/get_valid_filters.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ + +import { DataView } from '@kbn/data-views-plugin/common'; +import { Filter } from '@kbn/es-query'; + +export const getValidFilters = (dataView: DataView, filters: Filter[]): Filter[] => { + return filters.map((filter) => { + const meta = { ...filter.meta }; + + // We need to disable scripted filters that don't match this data view + // since we can't guarantee they'll succeed for the current data view + // and can lead to runtime errors + if (filter.query?.script && meta.index !== dataView.id) { + meta.disabled = true; + } + + return { ...filter, meta }; + }); +}; diff --git a/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts index 17103bc8783ffa..e7b8893848c930 100644 --- a/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts +++ b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts @@ -17,7 +17,7 @@ export const defaultEmbeddableFactoryProvider = < I extends EmbeddableInput = EmbeddableInput, O extends EmbeddableOutput = EmbeddableOutput, E extends IEmbeddable = IEmbeddable, - T extends SavedObjectAttributes = SavedObjectAttributes + T = SavedObjectAttributes >( def: EmbeddableFactoryDefinition ): EmbeddableFactory => { diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts index e1b7a48948743e..317ff4f773f232 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { SavedObjectAttributes } from '@kbn/core/public'; import { SavedObjectMetaData } from '@kbn/saved-objects-plugin/public'; import { PersistableState } from '@kbn/kibana-utils-plugin/common'; import { UiActionsPresentableGrouping } from '@kbn/ui-actions-plugin/public'; @@ -35,7 +34,7 @@ export interface EmbeddableFactory< TEmbeddableInput, TEmbeddableOutput >, - TSavedObjectAttributes extends SavedObjectAttributes = SavedObjectAttributes + TSavedObjectAttributes = unknown > extends PersistableState { // A unique identified for this factory, which will be used to map an embeddable spec to // a factory that can generate an instance of it. diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts index 2ae5d4161e15dc..06f3a268c9ae5f 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import type { SavedObjectAttributes } from '@kbn/core/server'; import { IEmbeddable } from './i_embeddable'; import { EmbeddableFactory } from './embeddable_factory'; import { EmbeddableInput, EmbeddableOutput } from '..'; @@ -15,7 +14,7 @@ export type EmbeddableFactoryDefinition< I extends EmbeddableInput = EmbeddableInput, O extends EmbeddableOutput = EmbeddableOutput, E extends IEmbeddable = IEmbeddable, - T extends SavedObjectAttributes = SavedObjectAttributes + T = unknown > = // Required parameters Pick, 'create' | 'type' | 'isEditable' | 'getDisplayName'> & diff --git a/src/plugins/embeddable/public/types.ts b/src/plugins/embeddable/public/types.ts index 29f38c2f1916bc..7f24f525ae49f2 100644 --- a/src/plugins/embeddable/public/types.ts +++ b/src/plugins/embeddable/public/types.ts @@ -34,7 +34,7 @@ export type EmbeddableFactoryProvider = < I extends EmbeddableInput = EmbeddableInput, O extends EmbeddableOutput = EmbeddableOutput, E extends IEmbeddable = IEmbeddable, - T extends SavedObjectAttributes = SavedObjectAttributes + T = SavedObjectAttributes >( def: EmbeddableFactoryDefinition ) => EmbeddableFactory; diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts index fdf6e99f7280de..ba767658e84919 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -import type { SavedObjectAttributes, SavedObjectsServiceSetup } from '@kbn/core/server'; +import type { SavedObjectsServiceSetup } from '@kbn/core/server'; /** * Used for accumulating the totals of all the stats older than 90d */ -export interface ApplicationUsageTotal extends SavedObjectAttributes { +export interface ApplicationUsageTotal { appId: string; viewId: string; minutesOnScreen: number; diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts index cf0c5372450e50..a980e0257e7e27 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts @@ -6,16 +6,12 @@ * Side Public License, v 1. */ -import type { - SavedObjectAttributes, - SavedObjectsServiceSetup, - ISavedObjectsRepository, -} from '@kbn/core/server'; +import type { SavedObjectsServiceSetup, ISavedObjectsRepository } from '@kbn/core/server'; import moment from 'moment'; import type { IntervalHistogram } from '@kbn/core/server'; export const SAVED_OBJECTS_DAILY_TYPE = 'event_loop_delays_daily'; -export interface EventLoopDelaysDaily extends SavedObjectAttributes, IntervalHistogram { +export interface EventLoopDelaysDaily extends IntervalHistogram { processId: number; instanceUuid: string; } diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts index 833e537b92ddfb..917d7a06a0f473 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts @@ -6,15 +6,11 @@ * Side Public License, v 1. */ -import { - ISavedObjectsRepository, - SavedObjectAttributes, - SavedObjectsServiceSetup, -} from '@kbn/core/server'; +import { ISavedObjectsRepository, SavedObjectsServiceSetup } from '@kbn/core/server'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { uiMetricSchema } from './schema'; -interface UIMetricsSavedObjects extends SavedObjectAttributes { +interface UIMetricsSavedObjects { count: number; } 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 44c281560fd545..94c869bb54a1c8 100644 --- a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx +++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx @@ -26,8 +26,8 @@ import { EuiToolTip, } from '@elastic/eui'; import type { DataViewListItem } from '@kbn/data-views-plugin/public'; -import { IDataPluginServices } from '@kbn/data-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { IUnifiedSearchPluginServices } from '../types'; import type { DataViewPickerPropsExtended } from '.'; import { DataViewsList } from './dataview_list'; import type { TextBasedLanguagesListProps } from './text_languages_list'; @@ -82,7 +82,7 @@ export function ChangeDataView({ const [isTextLangTransitionModalVisible, setIsTextLangTransitionModalVisible] = useState(false); const [selectedDataViewId, setSelectedDataViewId] = useState(currentDataViewId); - const kibana = useKibana(); + const kibana = useKibana(); const { application, data, storage } = kibana.services; const styles = changeDataViewStyles({ fullWidth: trigger.fullWidth }); const [isTextLangTransitionModalDismissed, setIsTextLangTransitionModalDismissed] = useState(() => diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.test.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.test.tsx index 87d0eca815cd80..cac21f97329046 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.test.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.test.tsx @@ -7,7 +7,8 @@ */ import { registerTestBed, TestBed } from '@kbn/test-jest-helpers'; -import { FilterEditor, Props } from '.'; +import type { FilterEditorProps } from '.'; +import { FilterEditor } from '.'; import React from 'react'; jest.mock('@kbn/kibana-react-plugin/public', () => { @@ -32,7 +33,7 @@ describe('', () => { let testBed: TestBed; beforeEach(async () => { - const defaultProps: Omit = { + const defaultProps: Omit = { filter: { meta: { type: 'phase', diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/index.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.tsx similarity index 90% rename from src/plugins/unified_search/public/filter_bar/filter_editor/index.tsx rename to src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.tsx index 0bdda65b32c9d0..cdf4af1746e5c9 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/index.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.tsx @@ -48,8 +48,9 @@ import { Operator } from './lib/filter_operators'; import { PhraseValueInput } from './phrase_value_input'; import { PhrasesValuesInput } from './phrases_values_input'; import { RangeValueInput } from './range_value_input'; +import { getFieldValidityAndErrorMessage } from './lib/helpers'; -export interface Props { +export interface FilterEditorProps { filter: Filter; indexPatterns: DataView[]; onSubmit: (filter: Filter) => void; @@ -84,8 +85,8 @@ const updateButtonLabel = i18n.translate('unifiedSearch.filter.filterEditor.upda defaultMessage: 'Update filter', }); -class FilterEditorUI extends Component { - constructor(props: Props) { +class FilterEditorUI extends Component { + constructor(props: FilterEditorProps) { super(props); this.state = { selectedIndexPattern: this.getIndexPatternFromFilter(), @@ -356,32 +357,55 @@ class FilterEditorUI extends Component { return ''; } + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage( + this.state.selectedField, + this.state.params + ); + switch (this.state.selectedOperator.type) { case 'exists': return ''; case 'phrase': return ( - + label={this.props.intl.formatMessage({ + id: 'unifiedSearch.filter.filterEditor.valueInputLabel', + defaultMessage: 'Value', + })} + isInvalid={isInvalid} + error={errorMessage} + > + + ); case 'phrases': return ( - + label={this.props.intl.formatMessage({ + id: 'unifiedSearch.filter.filterEditor.valuesSelectLabel', + defaultMessage: 'Values', + })} + > + + ); case 'range': return ( diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/index.ts b/src/plugins/unified_search/public/filter_bar/filter_editor/index.ts new file mode 100644 index 00000000000000..70b448a80dc33c --- /dev/null +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/index.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ + +export type { Operator } from './lib'; + +export { + getFieldFromFilter, + getOperatorFromFilter, + getFilterableFields, + getOperatorOptions, + validateParams, + isFilterValid, + isOperator, + isNotOperator, + isOneOfOperator, + isNotOneOfOperator, + isBetweenOperator, + isNotBetweenOperator, + existsOperator, + doesNotExistOperator, + FILTER_OPERATORS, +} from './lib'; + +export type { GenericComboBoxProps } from './generic_combo_box'; +export type { PhraseSuggestorProps } from './phrase_suggestor'; +export type { PhrasesSuggestorProps } from './phrases_values_input'; + +export { GenericComboBox } from './generic_combo_box'; +export { PhraseSuggestor } from './phrase_suggestor'; +export { PhrasesValuesInput } from './phrases_values_input'; +export { PhraseValueInput } from './phrase_value_input'; +export { RangeValueInput, isRangeParams } from './range_value_input'; +export { ValueInputType } from './value_input_type'; + +export { FilterEditor } from './filter_editor'; +export type { FilterEditorProps } from './filter_editor'; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.ts b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.ts index 0863d10fe0c104..b59ddcc424ff88 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.ts +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.ts @@ -37,7 +37,7 @@ export function getOperatorOptions(field: DataViewField) { } export function validateParams(params: any, field: DataViewField) { - switch (field.type) { + switch (field?.type) { case 'date': const moment = typeof params === 'string' ? dateMath.parse(params) : null; return Boolean(typeof params === 'string' && moment && moment.isValid()); diff --git a/src/plugins/unified_search/public/utils/helpers.test.ts b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/helpers.test.ts similarity index 100% rename from src/plugins/unified_search/public/utils/helpers.test.ts rename to src/plugins/unified_search/public/filter_bar/filter_editor/lib/helpers.test.ts diff --git a/src/plugins/unified_search/public/utils/helpers.ts b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/helpers.ts similarity index 92% rename from src/plugins/unified_search/public/utils/helpers.ts rename to src/plugins/unified_search/public/filter_bar/filter_editor/lib/helpers.ts index 6f0a605fa0e142..c0246168671f05 100644 --- a/src/plugins/unified_search/public/utils/helpers.ts +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/helpers.ts @@ -10,13 +10,13 @@ import type { DataViewField } from '@kbn/data-views-plugin/common'; import { i18n } from '@kbn/i18n'; import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; import { isEmpty } from 'lodash'; -import { validateParams } from '../filter_bar/filter_editor/lib/filter_editor_utils'; +import { validateParams } from './filter_editor_utils'; export const getFieldValidityAndErrorMessage = ( field: DataViewField, value?: string | undefined ): { isInvalid: boolean; errorMessage?: string } => { - const type = field.type; + const type = field?.type; switch (type) { case KBN_FIELD_TYPES.DATE: case KBN_FIELD_TYPES.DATE_RANGE: diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/index.ts b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/index.ts new file mode 100644 index 00000000000000..8ae6e6bd256078 --- /dev/null +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/index.ts @@ -0,0 +1,29 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +export { getFieldValidityAndErrorMessage } from './helpers'; +export type { Operator } from './filter_operators'; +export { + isOperator, + isNotOperator, + isOneOfOperator, + isNotOneOfOperator, + isBetweenOperator, + isNotBetweenOperator, + existsOperator, + doesNotExistOperator, + FILTER_OPERATORS, +} from './filter_operators'; +export { + getFieldFromFilter, + getOperatorFromFilter, + getFilterableFields, + getOperatorOptions, + validateParams, + isFilterValid, +} from './filter_editor_utils'; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_suggestor.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_suggestor.tsx index dc987421e26616..ac5af32203c919 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_suggestor.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_suggestor.tsx @@ -10,13 +10,12 @@ import React from 'react'; import { withKibana, KibanaReactContextValue } from '@kbn/kibana-react-plugin/public'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; -import { IDataPluginServices } from '@kbn/data-plugin/public'; import { debounce } from 'lodash'; -import { getAutocomplete } from '../../services'; +import { IUnifiedSearchPluginServices } from '../../types'; export interface PhraseSuggestorProps { - kibana: KibanaReactContextValue; + kibana: KibanaReactContextValue; indexPattern: DataView; field: DataViewField; timeRangeForSuggestionsOverride?: boolean; @@ -80,7 +79,7 @@ export class PhraseSuggestorUI extends React.Com return; } this.setState({ isLoading: true }); - const suggestions = await getAutocomplete().getValueSuggestions({ + const suggestions = await this.services.unifiedSearch.autocomplete.getValueSuggestions({ indexPattern, field, query, diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx index c0fe3dc4970255..210b201ec4c683 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { EuiFormRow } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n-react'; import { uniq } from 'lodash'; import React from 'react'; @@ -14,36 +13,27 @@ import { withKibana } from '@kbn/kibana-react-plugin/public'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor'; import { ValueInputType } from './value_input_type'; -import { getFieldValidityAndErrorMessage } from '../../utils/helpers'; -interface Props extends PhraseSuggestorProps { +interface PhraseValueInputProps extends PhraseSuggestorProps { value?: string; onChange: (value: string | number | boolean) => void; intl: InjectedIntl; fullWidth?: boolean; + compressed?: boolean; + disabled?: boolean; + isInvalid?: boolean; } -class PhraseValueInputUI extends PhraseSuggestorUI { +class PhraseValueInputUI extends PhraseSuggestorUI { public render() { - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage( - this.props.field, - this.props.value - ); - return ( - + <> {this.isSuggestingValues() ? ( this.renderWithSuggestions() ) : ( { value={this.props.value} onChange={this.props.onChange} field={this.props.field} - isInvalid={isInvalid} + isInvalid={this.props.isInvalid} /> )} - + ); } @@ -67,7 +57,9 @@ class PhraseValueInputUI extends PhraseSuggestorUI { const options = value ? uniq([valueAsStr, ...suggestions]) : suggestions; return ( void; onParamsUpdate: (value: string) => void; intl: InjectedIntl; fullWidth?: boolean; + compressed?: boolean; } -class PhrasesValuesInputUI extends PhraseSuggestorUI { +class PhrasesValuesInputUI extends PhraseSuggestorUI { public render() { const { suggestions } = this.state; - const { values, intl, onChange, fullWidth, onParamsUpdate } = this.props; + const { values, intl, onChange, fullWidth, onParamsUpdate, compressed } = this.props; const options = values ? uniq([...values, ...suggestions]) : suggestions; return ( - - option} - selectedOptions={values || []} - onSearchChange={this.onSearchChange} - onCreateOption={(option: string) => { - onParamsUpdate(option.trim()); - }} - onChange={onChange} - isClearable={false} - data-test-subj="filterParamsComboBox phrasesParamsComboxBox" - /> - + delimiter="," + options={options} + getLabel={(option) => option} + selectedOptions={values || []} + onSearchChange={this.onSearchChange} + onCreateOption={(option: string) => { + onParamsUpdate(option.trim()); + }} + onChange={onChange} + isClearable={false} + data-test-subj="filterParamsComboBox phrasesParamsComboxBox" + /> ); } } diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/range_value_input.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/range_value_input.tsx index 26a25886ac8666..27a1d9db7739d2 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/range_value_input.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/range_value_input.tsx @@ -28,6 +28,11 @@ interface Props { onChange: (params: RangeParamsPartial) => void; intl: InjectedIntl; fullWidth?: boolean; + compressed?: boolean; +} + +export function isRangeParams(params: any): params is RangeParams { + return Boolean(params && 'from' in params && 'to' in params); } function RangeValueInputUI(props: Props) { @@ -68,6 +73,7 @@ function RangeValueInputUI(props: Props) { startControl={ { @@ -37,12 +39,14 @@ class ValueInputTypeUI extends Component { public render() { const value = this.props.value; - const type = this.props.field.type; + const type = this.props.field?.type ?? 'string'; let inputElement: React.ReactNode; switch (type) { case 'string': inputElement = ( { case 'number_range': inputElement = ( { case 'date_range': inputElement = ( { inputElement = ( ); break; @@ -119,6 +129,7 @@ class ValueInputTypeUI extends Component { onChange={this.onBoolChange} className={this.props.className} fullWidth={this.props.fullWidth} + compressed={this.props.compressed} /> ); break; diff --git a/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx b/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx index abacffe4a46a9e..3f70a57708b465 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx @@ -27,9 +27,8 @@ import { getDisplayValueFromFilter, getFieldDisplayValueFromFilter, } from '@kbn/data-plugin/public'; -import { FilterEditor } from '../filter_editor'; +import { FilterEditor } from '../filter_editor/filter_editor'; import { FilterView } from '../filter_view'; -import { getIndexPatterns } from '../../services'; import { FilterPanelOption } from '../../types'; export interface FilterItemProps { @@ -67,7 +66,6 @@ export const FILTER_EDITOR_WIDTH = 800; export function FilterItem(props: FilterItemProps) { const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [indexPatternExists, setIndexPatternExists] = useState(undefined); const [renderedComponent, setRenderedComponent] = useState('menu'); const { id, filter, indexPatterns, hiddenPanelOptions, readOnly = false } = props; @@ -77,31 +75,6 @@ export function FilterItem(props: FilterItemProps) { } }, [isPopoverOpen]); - useEffect(() => { - const index = props.filter.meta.index; - let isSubscribed = true; - if (index) { - getIndexPatterns() - .get(index) - .then((indexPattern) => { - if (isSubscribed) { - setIndexPatternExists(!!indexPattern); - } - }) - .catch(() => { - if (isSubscribed) { - setIndexPatternExists(false); - } - }); - } else if (isSubscribed) { - // Allow filters without an index pattern and don't validate them. - setIndexPatternExists(true); - } - return () => { - isSubscribed = false; - }; - }, [props.filter.meta.index]); - function handleBadgeClick(e: MouseEvent) { if (e.shiftKey) { onToggleDisabled(); @@ -160,9 +133,8 @@ export function FilterItem(props: FilterItemProps) { function getDataTestSubj(labelConfig: LabelOptions) { const dataTestSubjKey = filter.meta.key ? `filter-key-${filter.meta.key}` : ''; - const dataTestSubjValue = filter.meta.value - ? `filter-value-${isValidLabel(labelConfig) ? labelConfig.title : labelConfig.status}` - : ''; + const valueLabel = isValidLabel(labelConfig) ? labelConfig.title : labelConfig.status; + const dataTestSubjValue = valueLabel ? `filter-value-${valueLabel}` : ''; const dataTestSubjNegated = filter.meta.negate ? 'filter-negated' : ''; const dataTestSubjDisabled = `filter-${isDisabled(labelConfig) ? 'disabled' : 'enabled'}`; const dataTestSubjPinned = `filter-${isFilterPinned(filter) ? 'pinned' : 'unpinned'}`; @@ -298,22 +270,7 @@ export function FilterItem(props: FilterItemProps) { return label; } - if (indexPatternExists === false) { - label.status = FILTER_ITEM_ERROR; - label.title = props.intl.formatMessage({ - id: 'unifiedSearch.filter.filterBar.labelErrorText', - defaultMessage: `Error`, - }); - label.message = props.intl.formatMessage( - { - id: 'unifiedSearch.filter.filterBar.labelErrorInfo', - defaultMessage: 'Index pattern {indexPattern} not found', - }, - { - indexPattern: filter.meta.index, - } - ); - } else if (isFilterApplicable()) { + if (isFilterApplicable()) { try { label.title = getDisplayValueFromFilter(filter, indexPatterns); } catch (e) { @@ -344,8 +301,6 @@ export function FilterItem(props: FilterItemProps) { return label; } - // Don't render until we know if the index pattern is valid - if (indexPatternExists === undefined) return null; const valueLabelConfig = getValueLabel(); // Disable errored filters and re-render diff --git a/src/plugins/unified_search/public/filter_bar/filter_item/filter_items.tsx b/src/plugins/unified_search/public/filter_bar/filter_item/filter_items.tsx index 4c29c4284860d3..c46a4973e2457e 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_item/filter_items.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_item/filter_items.tsx @@ -11,11 +11,11 @@ import { css } from '@emotion/react'; import { EuiFlexItem } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n-react'; import type { Filter } from '@kbn/es-query'; -import { IDataPluginServices } from '@kbn/data-plugin/public'; import { METRIC_TYPE } from '@kbn/analytics'; import { DataView } from '@kbn/data-views-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { FilterItem, FilterItemProps } from './filter_item'; +import type { IUnifiedSearchPluginServices } from '../../types'; /** * Properties for the filter items component, which will render a single filter pill for every filter that is sent in @@ -41,7 +41,7 @@ export interface FilterItemsProps { const FilterItemsUI = React.memo(function FilterItemsUI(props: FilterItemsProps) { const groupRef = useRef(null); - const kibana = useKibana(); + const kibana = useKibana(); const { appName, usageCollection, uiSettings } = kibana.services; const { readOnly = false } = props; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.tsx.snap b/src/plugins/unified_search/public/filter_bar/filter_label/__snapshots__/filter_label.test.tsx.snap similarity index 100% rename from src/plugins/unified_search/public/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.tsx.snap rename to src/plugins/unified_search/public/filter_bar/filter_label/__snapshots__/filter_label.test.tsx.snap diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.test.tsx b/src/plugins/unified_search/public/filter_bar/filter_label/filter_label.test.tsx similarity index 100% rename from src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.test.tsx rename to src/plugins/unified_search/public/filter_bar/filter_label/filter_label.test.tsx diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx b/src/plugins/unified_search/public/filter_bar/filter_label/filter_label.tsx similarity index 94% rename from src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx rename to src/plugins/unified_search/public/filter_bar/filter_label/filter_label.tsx index 35c05316465f80..261a2a6e7afb20 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_label/filter_label.tsx @@ -10,8 +10,8 @@ import React, { Fragment } from 'react'; import { EuiTextColor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Filter, FILTERS } from '@kbn/es-query'; -import { existsOperator, isOneOfOperator } from './filter_operators'; -import type { FilterLabelStatus } from '../../filter_item/filter_item'; +import type { FilterLabelStatus } from '../filter_item/filter_item'; +import { existsOperator, isOneOfOperator } from '../filter_editor'; export interface FilterLabelProps { filter: Filter; diff --git a/src/plugins/unified_search/public/filter_bar/index.tsx b/src/plugins/unified_search/public/filter_bar/index.tsx index a70b6b93de5dd1..a0fee65518fa85 100644 --- a/src/plugins/unified_search/public/filter_bar/index.tsx +++ b/src/plugins/unified_search/public/filter_bar/index.tsx @@ -29,7 +29,7 @@ export const FilterItems = (props: React.ComponentProps) ); -const LazyFilterLabel = React.lazy(() => import('./filter_editor/lib/filter_label')); +const LazyFilterLabel = React.lazy(() => import('./filter_label/filter_label')); /** * Renders the label for a single filter pill */ diff --git a/src/plugins/unified_search/public/filters_builder/__mock__/filters.ts b/src/plugins/unified_search/public/filters_builder/__mock__/filters.ts new file mode 100644 index 00000000000000..03d5b4333cff46 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/__mock__/filters.ts @@ -0,0 +1,630 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import type { Filter } from '@kbn/es-query'; + +export const getFiltersMock = () => + [ + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 1", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 1", + }, + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + type: 'OR', + params: [ + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 2", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 2", + }, + }, + $state: { + store: 'appState', + }, + }, + [ + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 3", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 3", + }, + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 4", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 4", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 5", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 5", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + }, + }, + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 6", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 6", + }, + }, + $state: { + store: 'appState', + }, + }, + ] as Filter[]; + +export const getFiltersMockOrHide = () => + [ + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 1", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 1", + }, + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 2", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 2", + }, + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 3", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 3", + }, + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 4", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 4", + }, + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 5", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 5", + }, + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 6", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 6", + }, + }, + $state: { + store: 'appState', + }, + }, + ] as Filter[]; + +export const getDataThatNeedsNormalized = () => + [ + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 1", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 1", + }, + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + type: 'OR', + params: [ + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 2", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 2", + }, + }, + $state: { + store: 'appState', + }, + }, + [ + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 3", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 3", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 5", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 5", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + }, + }, + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 6", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 6", + }, + }, + $state: { + store: 'appState', + }, + }, + ] as Filter[]; + +export const getDataAfterNormalized = () => + [ + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 1", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 1", + }, + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + type: 'OR', + params: [ + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 2", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 2", + }, + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 3", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 3", + }, + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 5", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 5", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + }, + }, + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 6", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 6", + }, + }, + $state: { + store: 'appState', + }, + }, + ] as Filter[]; + +export const getDataThatNeedNotNormalized = () => + [ + { + meta: { + type: 'OR', + params: [ + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 2", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 2", + }, + }, + $state: { + store: 'appState', + }, + }, + [ + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 3", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 3", + }, + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 4", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 4", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 5", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 5", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + }, + }, + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 6", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 6", + }, + }, + $state: { + store: 'appState', + }, + }, + ] as Filter[]; diff --git a/src/plugins/unified_search/public/filters_builder/__stories__/filter_builder.stories.tsx b/src/plugins/unified_search/public/filters_builder/__stories__/filter_builder.stories.tsx new file mode 100644 index 00000000000000..aebf85440dfe47 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/__stories__/filter_builder.stories.tsx @@ -0,0 +1,188 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React, { FC } from 'react'; +import { ComponentStory } from '@storybook/react'; +import { I18nProvider } from '@kbn/i18n-react'; +import { EuiForm } from '@elastic/eui'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { action } from '@storybook/addon-actions'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import type { Filter } from '@kbn/es-query'; +import { getFiltersMock, getFiltersMockOrHide } from '../__mock__/filters'; +import FiltersBuilder, { FiltersBuilderProps } from '../filters_builder'; + +export default { + title: 'Filters Builder', + component: FiltersBuilder, + decorators: [(story: Function) => {story()}], +}; + +const Template: ComponentStory> = (args) => ; + +export const Default = Template.bind({}); + +Default.decorators = [ + (Story) => ( + + + + + + ), +]; + +const mockedDataView = { + id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + title: 'logstash-*', + fields: [ + { + name: 'category.keyword', + type: 'string', + esTypes: ['integer'], + aggregatable: true, + filterable: true, + searchable: true, + }, + ], +} as DataView; + +const filters = getFiltersMock(); + +Default.args = { + filters, + dataView: mockedDataView, + onChange: (f: Filter[]) => {}, + hideOr: false, +}; + +export const withoutOR = Template.bind({}); +withoutOR.args = { ...Default.args, filters: getFiltersMockOrHide(), hideOr: true }; + +withoutOR.decorators = [ + (Story) => ( + + + + + + ), +]; + +const createMockWebStorage = () => ({ + clear: action('clear'), + getItem: action('getItem'), + key: action('key'), + removeItem: action('removeItem'), + setItem: action('setItem'), + length: 0, +}); + +const createMockStorage = () => ({ + storage: createMockWebStorage(), + set: action('set'), + remove: action('remove'), + clear: action('clear'), + get: () => true, +}); + +const services = { + uiSettings: { + get: () => true, + }, + savedObjects: action('savedObjects'), + notifications: action('notifications'), + http: { + basePath: { + prepend: () => 'http://test', + }, + }, + docLinks: { + links: { + query: { + kueryQuerySyntax: '', + }, + }, + }, + storage: createMockStorage(), + data: { + query: { + savedQueries: { + findSavedQueries: () => + Promise.resolve({ + queries: [ + { + id: 'testwewe', + attributes: { + title: 'Saved query 1', + description: '', + query: { + query: 'category.keyword : "Men\'s Shoes" ', + language: 'kuery', + }, + filters: [], + }, + }, + { + id: '0173d0d0-b19a-11ec-8323-837d6b231b82', + attributes: { + title: 'test', + description: '', + query: { + query: '', + language: 'kuery', + }, + filters: [ + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + }, + }, + ], + }), + }, + }, + dataViews: { + getIdsWithTitle: () => [ + { id: '8a0b7cd0-b0c4-11ec-92b2-73d62e0d28a9', title: 'logstash-*' }, + { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', title: 'test-*' }, + ], + }, + }, + unifiedSearch: { + autocomplete: { + hasQuerySuggestions: () => Promise.resolve(false), + getQuerySuggestions: () => [], + getValueSuggestions: () => + new Promise((resolve) => { + setTimeout(() => { + resolve([]); + }, 300); + }), + }, + }, +}; diff --git a/src/plugins/unified_search/public/filters_builder/assets/add.svg b/src/plugins/unified_search/public/filters_builder/assets/add.svg new file mode 100644 index 00000000000000..cfc9907424f623 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/assets/add.svg @@ -0,0 +1,33 @@ + + + add + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/plugins/unified_search/public/filters_builder/assets/or.svg b/src/plugins/unified_search/public/filters_builder/assets/or.svg new file mode 100644 index 00000000000000..d0be3ff2e77fe4 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/assets/or.svg @@ -0,0 +1,33 @@ + + + or + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder.tsx b/src/plugins/unified_search/public/filters_builder/filters_builder.tsx new file mode 100644 index 00000000000000..c7251bb78518c1 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filters_builder.tsx @@ -0,0 +1,126 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React, { useEffect, useReducer, useCallback, useState, useMemo } from 'react'; +import { EuiDragDropContext, DragDropContextProps, useEuiPaddingSize } from '@elastic/eui'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { Filter } from '@kbn/es-query'; +import { css } from '@emotion/css'; +import { FiltersBuilderContextType } from './filters_builder_context'; +import { ConditionTypes } from '../utils'; +import { FilterGroup } from './filters_builder_filter_group'; +import { FiltersBuilderReducer } from './filters_builder_reducer'; + +export interface FiltersBuilderProps { + filters: Filter[]; + dataView: DataView; + onChange: (filters: Filter[]) => void; + timeRangeForSuggestionsOverride?: boolean; + maxDepth?: number; + hideOr?: boolean; +} + +const rootLevelConditionType = ConditionTypes.AND; +const DEFAULT_MAX_DEPTH = 10; + +function FiltersBuilder({ + onChange, + dataView, + filters, + timeRangeForSuggestionsOverride, + maxDepth = DEFAULT_MAX_DEPTH, + hideOr = false, +}: FiltersBuilderProps) { + const [state, dispatch] = useReducer(FiltersBuilderReducer, { filters }); + const [dropTarget, setDropTarget] = useState(''); + const mPaddingSize = useEuiPaddingSize('m'); + + const filtersBuilderStyles = useMemo( + () => css` + .filter-builder__panel { + &.filter-builder__panel-nested { + padding: ${mPaddingSize} 0; + } + } + + .filter-builder__item { + &.filter-builder__item-nested { + padding: 0 ${mPaddingSize}; + } + } + `, + [mPaddingSize] + ); + + useEffect(() => { + if (state.filters !== filters) { + onChange(state.filters); + } + }, [filters, onChange, state.filters]); + + const handleMoveFilter = useCallback( + (pathFrom: string, pathTo: string, conditionalType: ConditionTypes) => { + if (pathFrom === pathTo) { + return null; + } + + dispatch({ + type: 'moveFilter', + payload: { + pathFrom, + pathTo, + conditionalType, + }, + }); + }, + [] + ); + + const onDragEnd: DragDropContextProps['onDragEnd'] = ({ combine, source, destination }) => { + if (source && destination) { + handleMoveFilter(source.droppableId, destination.droppableId, ConditionTypes.AND); + } + + if (source && combine) { + handleMoveFilter(source.droppableId, combine.droppableId, ConditionTypes.OR); + } + setDropTarget(''); + }; + + const onDragActive: DragDropContextProps['onDragUpdate'] = ({ destination, combine }) => { + if (destination) { + setDropTarget(destination.droppableId); + } + + if (combine) { + setDropTarget(combine.droppableId); + } + }; + + return ( +
+ + + + + +
+ ); +} + +// React.lazy support +// eslint-disable-next-line import/no-default-export +export default FiltersBuilder; diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_context.ts b/src/plugins/unified_search/public/filters_builder/filters_builder_context.ts new file mode 100644 index 00000000000000..8dfab23f978873 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filters_builder_context.ts @@ -0,0 +1,26 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React, { Dispatch } from 'react'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { FiltersBuilderActions } from './filters_builder_reducer'; + +interface FiltersBuilderContextType { + dataView: DataView; + dispatch: Dispatch; + globalParams: { + maxDepth: number; + hideOr: boolean; + }; + dropTarget: string; + timeRangeForSuggestionsOverride?: boolean; +} + +export const FiltersBuilderContextType = React.createContext( + {} as FiltersBuilderContextType +); diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_filter_group.tsx b/src/plugins/unified_search/public/filters_builder/filters_builder_filter_group.tsx new file mode 100644 index 00000000000000..adb77f7b9d2eac --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filters_builder_filter_group.tsx @@ -0,0 +1,149 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React, { useContext, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiPanel, + EuiText, + useEuiBackgroundColor, + useEuiPaddingSize, +} from '@elastic/eui'; +import { Filter } from '@kbn/es-query'; +import { css, cx } from '@emotion/css'; +import type { Path } from './filters_builder_types'; +import { ConditionTypes, getConditionalOperationType } from '../utils'; +import { FilterItem } from './filters_builder_filter_item'; +import { FiltersBuilderContextType } from './filters_builder_context'; +import { getPathInArray } from './filters_builder_utils'; + +export interface FilterGroupProps { + filters: Filter[]; + conditionType: ConditionTypes; + path: Path; + + /** @internal used for recursive rendering **/ + renderedLevel?: number; + reverseBackground?: boolean; +} + +/** @internal **/ +const Delimiter = ({ + color, + conditionType, +}: { + color: 'subdued' | 'plain'; + conditionType: ConditionTypes; +}) => { + const xsPadding = useEuiPaddingSize('xs'); + const mPadding = useEuiPaddingSize('m'); + const backgroundColor = useEuiBackgroundColor(color); + + const delimiterStyles = useMemo( + () => css` + position: relative; + + .filter-builder__delimiter_text { + position: absolute; + display: block; + padding: ${xsPadding}; + top: 0; + left: ${mPadding}; + background: ${backgroundColor}; + } + `, + [backgroundColor, mPadding, xsPadding] + ); + + return ( +
+ + + {i18n.translate('unifiedSearch.filter.filtersBuilder.delimiterLabel', { + defaultMessage: '{conditionType}', + values: { + conditionType, + }, + })} + +
+ ); +}; + +export const FilterGroup = ({ + filters, + conditionType, + path, + reverseBackground = false, + renderedLevel = 0, +}: FilterGroupProps) => { + const { + globalParams: { maxDepth, hideOr }, + } = useContext(FiltersBuilderContextType); + + const pathInArray = getPathInArray(path); + const isDepthReached = maxDepth <= pathInArray.length; + const orDisabled = hideOr || (isDepthReached && conditionType === ConditionTypes.AND); + const andDisabled = isDepthReached && conditionType === ConditionTypes.OR; + const removeDisabled = pathInArray.length <= 1 && filters.length === 1; + const shouldNormalizeFirstLevel = + !path && filters.length === 1 && getConditionalOperationType(filters[0]); + + if (shouldNormalizeFirstLevel) { + reverseBackground = true; + renderedLevel -= 1; + } + + const color = reverseBackground ? 'plain' : 'subdued'; + + const renderedFilters = filters.map((filter, index, acc) => ( + + + + + + {conditionType && index + 1 < acc.length ? ( + + {conditionType === ConditionTypes.OR && ( + + )} + + ) : null} + + )); + + return shouldNormalizeFirstLevel ? ( + <>{renderedFilters} + ) : ( + 0, + })} + > + {renderedFilters} + + ); +}; diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item.tsx b/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item.tsx new file mode 100644 index 00000000000000..30b73d397b674f --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item.tsx @@ -0,0 +1,317 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useContext, useMemo } from 'react'; +import { + EuiButtonIcon, + EuiDraggable, + EuiDroppable, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIcon, + EuiPanel, + useEuiTheme, +} from '@elastic/eui'; +import { buildEmptyFilter, FieldFilter, Filter, getFilterParams } from '@kbn/es-query'; +import { DataViewField } from '@kbn/data-views-plugin/common'; +import { i18n } from '@kbn/i18n'; +import { cx, css } from '@emotion/css'; + +import add from '../assets/add.svg'; +import or from '../assets/or.svg'; + +import { FieldInput } from './filters_builder_filter_item_field_input'; +import { OperatorInput } from './filters_builder_filter_item_operator_input'; +import { ParamsEditor } from './filters_builder_filter_item_params_editor'; +import { ConditionTypes, getConditionalOperationType } from '../../utils'; +import { FiltersBuilderContextType } from '../filters_builder_context'; +import { FilterGroup } from '../filters_builder_filter_group'; +import type { Path } from '../filters_builder_types'; +import { getFieldFromFilter, getOperatorFromFilter } from '../../filter_bar/filter_editor'; +import { Operator } from '../../filter_bar/filter_editor'; + +export interface FilterItemProps { + path: Path; + filter: Filter; + disableOr: boolean; + disableAnd: boolean; + disableRemove: boolean; + color: 'plain' | 'subdued'; + index: number; + + /** @internal used for recursive rendering **/ + renderedLevel: number; + reverseBackground: boolean; +} + +const cursorAddStyles = css` + cursor: url(${add}), auto; +`; + +const cursorOrStyles = css` + cursor: url(${or}), auto; +`; + +export function FilterItem({ + filter, + path, + reverseBackground, + disableOr, + disableAnd, + disableRemove, + color, + index, + renderedLevel, +}: FilterItemProps) { + const { + dispatch, + dataView, + dropTarget, + globalParams: { hideOr }, + timeRangeForSuggestionsOverride, + } = useContext(FiltersBuilderContextType); + const conditionalOperationType = getConditionalOperationType(filter); + const { euiTheme } = useEuiTheme(); + + const grabIconStyles = useMemo( + () => css` + margin: 0 ${euiTheme.size.xxs}; + `, + [euiTheme.size.xxs] + ); + + let field: DataViewField | undefined; + let operator: Operator | undefined; + let params: Filter['meta']['params'] | undefined; + + if (!conditionalOperationType) { + field = getFieldFromFilter(filter as FieldFilter, dataView); + operator = getOperatorFromFilter(filter); + params = getFilterParams(filter); + } + + const onHandleField = useCallback( + (selectedField: DataViewField) => { + dispatch({ + type: 'updateFilter', + payload: { path, field: selectedField }, + }); + }, + [dispatch, path] + ); + + const onHandleOperator = useCallback( + (selectedOperator: Operator) => { + dispatch({ + type: 'updateFilter', + payload: { path, field, operator: selectedOperator }, + }); + }, + [dispatch, path, field] + ); + + const onHandleParamsChange = useCallback( + (selectedParams: string) => { + dispatch({ + type: 'updateFilter', + payload: { path, field, operator, params: selectedParams }, + }); + }, + [dispatch, path, field, operator] + ); + + const onHandleParamsUpdate = useCallback( + (value: Filter['meta']['params']) => { + dispatch({ + type: 'updateFilter', + payload: { path, params: [value, ...(params || [])] }, + }); + }, + [dispatch, path, params] + ); + + const onRemoveFilter = useCallback(() => { + dispatch({ + type: 'removeFilter', + payload: { + path, + }, + }); + }, [dispatch, path]); + + const onAddFilter = useCallback( + (conditionalType: ConditionTypes) => { + dispatch({ + type: 'addFilter', + payload: { + path, + filter: buildEmptyFilter(false, dataView.id), + conditionalType, + }, + }); + }, + [dispatch, dataView.id, path] + ); + + const onAddButtonClick = useCallback(() => onAddFilter(ConditionTypes.AND), [onAddFilter]); + const onOrButtonClick = useCallback(() => onAddFilter(ConditionTypes.OR), [onAddFilter]); + + if (!dataView) { + return null; + } + + return ( +
0, + })} + > + {conditionalOperationType ? ( + + ) : ( + + + {(provided) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {!hideOr ? ( + + + + ) : null} + + + + + + + + + + )} + + + )} +
+ ); +} diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item_field_input.tsx b/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item_field_input.tsx new file mode 100644 index 00000000000000..3ff823a09cb5de --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item_field_input.tsx @@ -0,0 +1,51 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import { useGeneratedHtmlId } from '@elastic/eui'; +import { getFilterableFields, GenericComboBox } from '../../filter_bar/filter_editor'; + +interface FieldInputProps { + dataView: DataView; + onHandleField: (field: DataViewField) => void; + field?: DataViewField; +} + +export function FieldInput({ field, dataView, onHandleField }: FieldInputProps) { + const fields = dataView ? getFilterableFields(dataView) : []; + const id = useGeneratedHtmlId({ prefix: 'fieldInput' }); + + const onFieldChange = useCallback( + ([selectedField]: DataViewField[]) => { + onHandleField(selectedField); + }, + [onHandleField] + ); + + const getLabel = useCallback((view: DataViewField) => view.customLabel || view.name, []); + + return ( + + ); +} diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item_operator_input.tsx b/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item_operator_input.tsx new file mode 100644 index 00000000000000..2f73df35962127 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item_operator_input.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; +import type { Operator } from '../../filter_bar/filter_editor'; +import { getOperatorOptions, GenericComboBox } from '../../filter_bar/filter_editor'; + +interface OperatorInputProps { + field: DataViewField | undefined; + operator: Operator | undefined; + params: TParams; + onHandleOperator: (operator: Operator, params?: TParams) => void; +} + +export function OperatorInput({ + field, + operator, + params, + onHandleOperator, +}: OperatorInputProps) { + const operators = field ? getOperatorOptions(field) : []; + + const onOperatorChange = useCallback( + ([selectedOperator]: Operator[]) => { + const selectedParams = selectedOperator === operator ? params : undefined; + + onHandleOperator(selectedOperator, selectedParams); + }, + [onHandleOperator, operator, params] + ); + + return ( + message} + onChange={onOperatorChange} + singleSelection={{ asPlainText: true }} + isClearable={false} + /> + ); +} diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item_params_editor.tsx b/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item_params_editor.tsx new file mode 100644 index 00000000000000..17c571ac7ed39a --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item_params_editor.tsx @@ -0,0 +1,113 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback } from 'react'; +import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import { EuiFormRow } from '@elastic/eui'; +import type { Operator } from '../../filter_bar/filter_editor'; +import { + PhraseValueInput, + PhrasesValuesInput, + RangeValueInput, + isRangeParams, +} from '../../filter_bar/filter_editor'; +import { getFieldValidityAndErrorMessage } from '../../filter_bar/filter_editor/lib'; + +interface ParamsEditorProps { + dataView: DataView; + params: TParams; + onHandleParamsChange: (params: TParams) => void; + onHandleParamsUpdate: (value: TParams) => void; + timeRangeForSuggestionsOverride?: boolean; + field?: DataViewField; + operator?: Operator; +} + +export function ParamsEditor({ + dataView, + field, + operator, + params, + onHandleParamsChange, + onHandleParamsUpdate, + timeRangeForSuggestionsOverride, +}: ParamsEditorProps) { + const onParamsChange = useCallback( + (selectedParams) => { + onHandleParamsChange(selectedParams); + }, + [onHandleParamsChange] + ); + + const onParamsUpdate = useCallback( + (value) => { + onHandleParamsUpdate(value); + }, + [onHandleParamsUpdate] + ); + + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage( + field!, + typeof params === 'string' ? params : undefined + ); + + switch (operator?.type) { + case 'exists': + return null; + case 'phrase': + return ( + + + + ); + case 'phrases': + return ( + + ); + case 'range': + return ( + + ); + default: + return ( + + ); + } +} diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/index.ts b/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/index.ts new file mode 100644 index 00000000000000..07dd57964a13e0 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ + +export { FilterItem } from './filters_builder_filter_item'; diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_reducer.ts b/src/plugins/unified_search/public/filters_builder/filters_builder_reducer.ts new file mode 100644 index 00000000000000..3dde3bdddac67b --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filters_builder_reducer.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ + +import type { Reducer } from 'react'; +import type { Filter } from '@kbn/es-query'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; +import type { Path } from './filters_builder_types'; +import type { ConditionTypes } from '../utils'; +import { addFilter, moveFilter, removeFilter, updateFilter } from './filters_builder_utils'; +import type { Operator } from '../filter_bar/filter_editor'; + +/** @internal **/ +export interface FiltersBuilderState { + filters: Filter[]; +} + +/** @internal **/ +export interface AddFilterPayload { + path: Path; + filter: Filter; + conditionalType: ConditionTypes; +} + +/** @internal **/ +export interface UpdateFilterPayload { + path: string; + field?: DataViewField; + operator?: Operator; + params?: Filter['meta']['params']; +} + +/** @internal **/ +export interface RemoveFilterPayload { + path: Path; +} + +/** @internal **/ +export interface MoveFilterPayload { + pathFrom: Path; + pathTo: Path; + conditionalType: ConditionTypes; +} + +/** @internal **/ +export type FiltersBuilderActions = + | { type: 'addFilter'; payload: AddFilterPayload } + | { type: 'removeFilter'; payload: RemoveFilterPayload } + | { type: 'moveFilter'; payload: MoveFilterPayload } + | { type: 'updateFilter'; payload: UpdateFilterPayload }; + +export const FiltersBuilderReducer: Reducer = ( + state, + action +) => { + switch (action.type) { + case 'addFilter': + return { + filters: addFilter( + state.filters, + action.payload.filter, + action.payload.path, + action.payload.conditionalType + ), + }; + case 'removeFilter': + return { + ...state, + filters: removeFilter(state.filters, action.payload.path), + }; + case 'moveFilter': + return { + ...state, + filters: moveFilter( + state.filters, + action.payload.pathFrom, + action.payload.pathTo, + action.payload.conditionalType + ), + }; + case 'updateFilter': + return { + ...state, + filters: updateFilter( + state.filters, + action.payload.path, + action.payload.field, + action.payload.operator, + action.payload.params + ), + }; + default: + throw new Error('wrong action'); + } +}; diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_types.ts b/src/plugins/unified_search/public/filters_builder/filters_builder_types.ts new file mode 100644 index 00000000000000..24d0b9015aa749 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filters_builder_types.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 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 or the Server + * Side Public License, v 1. + */ + +/** @internal **/ +export type Path = string; diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_utils.test.ts b/src/plugins/unified_search/public/filters_builder/filters_builder_utils.test.ts new file mode 100644 index 00000000000000..517a0cea4cce7a --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filters_builder_utils.test.ts @@ -0,0 +1,261 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { buildEmptyFilter, Filter } from '@kbn/es-query'; +import { ConditionTypes } from '../utils'; +import { + getFilterByPath, + getPathInArray, + addFilter, + removeFilter, + moveFilter, + normalizeFilters, +} from './filters_builder_utils'; +import type { FilterItem } from '../utils'; +import { getConditionalOperationType } from '../utils'; + +import { + getDataAfterNormalized, + getDataThatNeedNotNormalized, + getDataThatNeedsNormalized, + getFiltersMock, +} from './__mock__/filters'; + +describe('filters_builder_utils', () => { + let filters: Filter[]; + beforeAll(() => { + filters = getFiltersMock(); + }); + + describe('getFilterByPath', () => { + test('should return correct filterByPath', () => { + expect(getFilterByPath(filters, '0')).toMatchInlineSnapshot(` + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "key": "category.keyword", + "negate": false, + "params": Object { + "query": "Men's Accessories 1", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "category.keyword": "Men's Accessories 1", + }, + }, + } + `); + expect(getFilterByPath(filters, '2')).toMatchInlineSnapshot(` + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "key": "category.keyword", + "negate": false, + "params": Object { + "query": "Men's Accessories 6", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "category.keyword": "Men's Accessories 6", + }, + }, + } + `); + expect(getFilterByPath(filters, '1.2')).toMatchInlineSnapshot(` + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "key": "category.keyword", + "negate": false, + "params": Object { + "query": "Men's Accessories 5", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "category.keyword": "Men's Accessories 5", + }, + }, + } + `); + expect(getFilterByPath(filters, '1.1.1')).toMatchInlineSnapshot(` + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "key": "category.keyword", + "negate": false, + "params": Object { + "query": "Men's Accessories 4", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "category.keyword": "Men's Accessories 4", + }, + }, + } + `); + expect(getFilterByPath(filters, '1.1')).toMatchInlineSnapshot(` + Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "key": "category.keyword", + "negate": false, + "params": Object { + "query": "Men's Accessories 3", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "category.keyword": "Men's Accessories 3", + }, + }, + }, + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "key": "category.keyword", + "negate": false, + "params": Object { + "query": "Men's Accessories 4", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "category.keyword": "Men's Accessories 4", + }, + }, + }, + ] + `); + }); + }); + + describe('getConditionalOperationType', () => { + let filter: Filter; + let filtersWithOrRelationships: FilterItem; + let groupOfFilters: FilterItem; + + beforeAll(() => { + filter = filters[0]; + filtersWithOrRelationships = filters[1]; + groupOfFilters = filters[1].meta.params; + }); + + test('should return correct ConditionalOperationType', () => { + expect(getConditionalOperationType(filter)).toBeUndefined(); + expect(getConditionalOperationType(filtersWithOrRelationships)).toBe(ConditionTypes.OR); + expect(getConditionalOperationType(groupOfFilters)).toBe(ConditionTypes.AND); + }); + }); + + describe('getPathInArray', () => { + test('should return correct path in array from path', () => { + expect(getPathInArray('0')).toStrictEqual([0]); + expect(getPathInArray('1.1')).toStrictEqual([1, 1]); + expect(getPathInArray('1.0.2')).toStrictEqual([1, 0, 2]); + }); + }); + + describe('addFilter', () => { + const emptyFilter = buildEmptyFilter(false); + + test('should add filter into filters after zero element', () => { + const enlargedFilters = addFilter(filters, emptyFilter, '0', ConditionTypes.AND); + expect(getFilterByPath(enlargedFilters, '1')).toMatchInlineSnapshot(` + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": undefined, + "negate": false, + }, + } + `); + }); + }); + + describe('removeFilter', () => { + test('should remove filter from filters', () => { + const path = '1.1'; + const filterBeforeRemoved = getFilterByPath(filters, path); + const filtersAfterRemoveFilter = removeFilter(filters, path); + const filterObtainedAfterFilterRemovalFromFilters = getFilterByPath( + filtersAfterRemoveFilter, + path + ); + + expect(filterBeforeRemoved).not.toBe(filterObtainedAfterFilterRemovalFromFilters); + }); + }); + + describe('moveFilter', () => { + test('should move filter from "0" path to "2" path into filters', () => { + const filterBeforeMoving = getFilterByPath(filters, '0'); + const filtersAfterMovingFilter = moveFilter(filters, '0', '2', ConditionTypes.AND); + const filterObtainedAfterFilterMovingFilters = getFilterByPath(filtersAfterMovingFilter, '2'); + expect(filterBeforeMoving).toEqual(filterObtainedAfterFilterMovingFilters); + }); + }); + + describe('normalizeFilters', () => { + test('should normalize filter after removed filter', () => { + const dataNeedsNormalized = getDataThatNeedsNormalized(); + const dataAfterNormalized = getDataAfterNormalized(); + expect(normalizeFilters(dataNeedsNormalized)).toEqual(dataAfterNormalized); + }); + + test('should not normalize filter after removed filter', () => { + const dataNeedNotNormalized = getDataThatNeedNotNormalized(); + const dataAfterNormalized = getDataThatNeedNotNormalized(); + expect(normalizeFilters(dataNeedNotNormalized)).toEqual(dataAfterNormalized); + }); + }); +}); diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_utils.ts b/src/plugins/unified_search/public/filters_builder/filters_builder_utils.ts new file mode 100644 index 00000000000000..dbbc81824a479f --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filters_builder_utils.ts @@ -0,0 +1,360 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { DataViewField } from '@kbn/data-views-plugin/common'; +import type { Filter } from '@kbn/es-query'; +import { cloneDeep } from 'lodash'; +import { ConditionTypes, getConditionalOperationType, isOrFilter, buildOrFilter } from '../utils'; +import type { FilterItem } from '../utils'; +import type { Operator } from '../filter_bar/filter_editor'; + +const PATH_SEPARATOR = '.'; + +/** + * The method returns the filter nesting identification number as an array. + * @param {string} path - variable is used to identify the filter and its nesting in the filter group. + */ +export const getPathInArray = (path: string) => path.split(PATH_SEPARATOR).map((i) => +i); + +const getGroupedFilters = (filter: FilterItem) => + Array.isArray(filter) ? filter : filter?.meta?.params; + +const doForFilterByPath = ( + filters: FilterItem[], + path: string, + action: (filter: FilterItem) => T +) => { + const pathArray = getPathInArray(path); + let f = filters[pathArray[0]]; + for (let i = 1, depth = pathArray.length; i < depth; i++) { + f = getGroupedFilters(f)[+pathArray[i]]; + } + return action(f); +}; + +const getContainerMetaByPath = (filters: FilterItem[], pathInArray: number[]) => { + let targetArray: FilterItem[] = filters; + let parentFilter: FilterItem | undefined; + let parentConditionType = ConditionTypes.AND; + + if (pathInArray.length > 1) { + parentFilter = getFilterByPath(filters, getParentFilterPath(pathInArray)); + parentConditionType = getConditionalOperationType(parentFilter) ?? parentConditionType; + targetArray = getGroupedFilters(parentFilter); + } + + return { + parentFilter, + targetArray, + parentConditionType, + }; +}; + +const getParentFilterPath = (pathInArray: number[]) => + pathInArray.slice(0, -1).join(PATH_SEPARATOR); + +/** + * The method corrects the positions of the filters after removing some filter from the filters. + * @param {FilterItem[]} filters - an array of filters that may contain filters that are incorrectly nested for later display in the UI. + */ +export const normalizeFilters = (filters: FilterItem[]) => { + const doRecursive = (f: FilterItem, parent: FilterItem) => { + if (Array.isArray(f)) { + return normalizeArray(f, parent); + } else if (isOrFilter(f)) { + return normalizeOr(f); + } + return f; + }; + + const normalizeArray = (filtersArray: FilterItem[], parent: FilterItem): FilterItem[] => { + const partiallyNormalized = filtersArray + .map((item) => { + const normalized = doRecursive(item, filtersArray); + + if (Array.isArray(normalized)) { + if (normalized.length === 1) { + return normalized[0]; + } + if (normalized.length === 0) { + return undefined; + } + } + return normalized; + }, []) + .filter(Boolean) as FilterItem[]; + + return Array.isArray(parent) ? partiallyNormalized.flat() : partiallyNormalized; + }; + + const normalizeOr = (orFilter: Filter): FilterItem => { + const orFilters = getGroupedFilters(orFilter); + if (orFilters.length < 2) { + return orFilters[0]; + } + + return { + ...orFilter, + meta: { + ...orFilter.meta, + params: doRecursive(orFilters, orFilter), + }, + }; + }; + + return normalizeArray(filters, filters) as Filter[]; +}; + +/** + * Find filter by path. + * @param {FilterItem[]} filters - filters in which the search for the desired filter will occur. + * @param {string} path - path to filter. + */ +export const getFilterByPath = (filters: FilterItem[], path: string) => + doForFilterByPath(filters, path, (f) => f); + +/** + * Method to add a filter to a specified location in a filter group. + * @param {Filter[]} filters - array of filters where the new filter will be added. + * @param {FilterItem} filter - new filter. + * @param {string} path - path to filter. + * @param {ConditionTypes} conditionalType - OR/AND relationships between filters. + */ +export const addFilter = ( + filters: Filter[], + filter: FilterItem, + path: string, + conditionalType: ConditionTypes +) => { + const newFilters = cloneDeep(filters); + const pathInArray = getPathInArray(path); + const { targetArray, parentConditionType } = getContainerMetaByPath(newFilters, pathInArray); + const selector = pathInArray[pathInArray.length - 1]; + + if (parentConditionType !== conditionalType) { + if (conditionalType === ConditionTypes.OR) { + targetArray.splice(selector, 1, buildOrFilter([targetArray[selector], filter])); + } + if (conditionalType === ConditionTypes.AND) { + targetArray.splice(selector, 1, [targetArray[selector], filter]); + } + } else { + targetArray.splice(selector + 1, 0, filter); + } + + return newFilters; +}; + +/** + * Remove filter from specified location. + * @param {Filter[]} filters - array of filters. + * @param {string} path - path to filter. + */ +export const removeFilter = (filters: Filter[], path: string) => { + const newFilters = cloneDeep(filters); + const pathInArray = getPathInArray(path); + const { targetArray } = getContainerMetaByPath(newFilters, pathInArray); + const selector = pathInArray[pathInArray.length - 1]; + + targetArray.splice(selector, 1); + + return normalizeFilters(newFilters); +}; + +/** + * Moving the filter on drag and drop. + * @param {Filter[]} filters - array of filters. + * @param {string} from - filter path before moving. + * @param {string} to - filter path where the filter will be moved. + * @param {ConditionTypes} conditionalType - OR/AND relationships between filters. + */ +export const moveFilter = ( + filters: Filter[], + from: string, + to: string, + conditionalType: ConditionTypes +) => { + const addFilterThenRemoveFilter = ( + source: Filter[], + addedFilter: FilterItem, + pathFrom: string, + pathTo: string, + conditional: ConditionTypes + ) => { + const newFiltersWithFilter = addFilter(source, addedFilter, pathTo, conditional); + return removeFilter(newFiltersWithFilter, pathFrom); + }; + + const removeFilterThenAddFilter = ( + source: Filter[], + removableFilter: FilterItem, + pathFrom: string, + pathTo: string, + conditional: ConditionTypes + ) => { + const newFiltersWithoutFilter = removeFilter(source, pathFrom); + return addFilter(newFiltersWithoutFilter, removableFilter, pathTo, conditional); + }; + + const newFilters = cloneDeep(filters); + const movingFilter = getFilterByPath(newFilters, from); + + const pathInArrayTo = getPathInArray(to); + const pathInArrayFrom = getPathInArray(from); + + if (pathInArrayTo.length === pathInArrayFrom.length) { + const filterPositionTo = pathInArrayTo.at(-1); + const filterPositionFrom = pathInArrayFrom.at(-1); + + const { parentConditionType } = getContainerMetaByPath(newFilters, pathInArrayTo); + const filterMovementDirection = Number(filterPositionTo) - Number(filterPositionFrom); + + if (filterMovementDirection === -1 && parentConditionType === conditionalType) { + return filters; + } + + if (filterMovementDirection >= -1) { + return addFilterThenRemoveFilter(newFilters, movingFilter, from, to, conditionalType); + } else { + return removeFilterThenAddFilter(newFilters, movingFilter, from, to, conditionalType); + } + } + + if (pathInArrayTo.length > pathInArrayFrom.length) { + return addFilterThenRemoveFilter(newFilters, movingFilter, from, to, conditionalType); + } else { + return removeFilterThenAddFilter(newFilters, movingFilter, from, to, conditionalType); + } +}; + +/** + * Method to update values inside filter. + * @param {Filter[]} filters - filter array + * @param {string} path - path to filter + * @param {DataViewField} field - DataViewField property inside a filter + * @param {Operator} operator - defines a relation by property and value + * @param {Filter['meta']['params']} params - filter value + */ +export const updateFilter = ( + filters: Filter[], + path: string, + field?: DataViewField, + operator?: Operator, + params?: Filter['meta']['params'] +) => { + const newFilters = [...filters]; + const changedFilter = getFilterByPath(newFilters, path) as Filter; + let filter = Object.assign({}, changedFilter); + + if (field && operator && params) { + if (Array.isArray(params)) { + filter = updateWithIsOneOfOperator(filter, operator, params); + } else { + filter = updateWithIsOperator(filter, operator, params); + } + } else if (field && operator) { + if (operator.type === 'exists') { + filter = updateWithExistsOperator(filter, operator); + } else { + filter = updateOperator(filter, operator); + } + } else { + filter = updateField(filter, field); + } + + const pathInArray = getPathInArray(path); + const { targetArray } = getContainerMetaByPath(newFilters, pathInArray); + const selector = pathInArray[pathInArray.length - 1]; + targetArray.splice(selector, 1, filter); + + return newFilters; +}; + +function updateField(filter: Filter, field?: DataViewField) { + return { + ...filter, + meta: { + ...filter.meta, + key: field?.name, + params: { query: undefined }, + value: undefined, + type: undefined, + }, + query: undefined, + }; +} + +function updateOperator(filter: Filter, operator?: Operator) { + return { + ...filter, + meta: { + ...filter.meta, + negate: operator?.negate, + type: operator?.type, + params: { ...filter.meta.params, query: undefined }, + value: undefined, + }, + query: { match_phrase: { field: filter.meta.key } }, + }; +} + +function updateWithExistsOperator(filter: Filter, operator?: Operator) { + return { + ...filter, + meta: { + ...filter.meta, + negate: operator?.negate, + type: operator?.type, + params: undefined, + value: 'exists', + }, + query: { exists: { field: filter.meta.key } }, + }; +} + +function updateWithIsOperator( + filter: Filter, + operator?: Operator, + params?: Filter['meta']['params'] +) { + return { + ...filter, + meta: { + ...filter.meta, + negate: operator?.negate, + type: operator?.type, + params: { ...filter.meta.params, query: params }, + }, + query: { match_phrase: { ...filter!.query?.match_phrase, [filter.meta.key!]: params } }, + }; +} + +function updateWithIsOneOfOperator( + filter: Filter, + operator?: Operator, + params?: Array +) { + return { + ...filter, + meta: { + ...filter.meta, + negate: operator?.negate, + type: operator?.type, + params, + }, + query: { + bool: { + minimum_should_match: 1, + ...filter!.query?.should, + should: params?.map((param) => { + return { match_phrase: { [filter.meta.key!]: param } }; + }), + }, + }, + }; +} diff --git a/src/plugins/unified_search/public/filters_builder/index.ts b/src/plugins/unified_search/public/filters_builder/index.ts new file mode 100644 index 00000000000000..0f430ca87aaac9 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { withSuspense } from '@kbn/shared-ux-utility'; + +/** + * The Lazily-loaded `FiltersBuilder` component. Consumers should use `React.Suspense` or + * the withSuspense` HOC to load this component. + */ +export const FiltersBuilderLazy = React.lazy(() => import('./filters_builder')); + +/** + * A `FiltersBuilder` component that is wrapped by the `withSuspense` HOC. This component can + * be used directly by consumers and will load the `FiltersBuilderLazy` component lazily with + * a predefined fallback and error boundary. + */ +export const FiltersBuilder = withSuspense(FiltersBuilderLazy); diff --git a/src/plugins/unified_search/public/index.ts b/src/plugins/unified_search/public/index.ts index 131b4450173537..731396014d8354 100755 --- a/src/plugins/unified_search/public/index.ts +++ b/src/plugins/unified_search/public/index.ts @@ -12,7 +12,11 @@ export type { IndexPatternSelectProps } from './index_pattern_select'; export type { QueryStringInputProps } from './query_string_input'; export { QueryStringInput } from './query_string_input'; export type { StatefulSearchBarProps, SearchBarProps } from './search_bar'; -export type { UnifiedSearchPublicPluginStart, UnifiedSearchPluginSetup } from './types'; +export type { + UnifiedSearchPublicPluginStart, + UnifiedSearchPluginSetup, + IUnifiedSearchPluginServices, +} from './types'; export { SearchBar } from './search_bar'; export type { FilterItemsProps } from './filter_bar'; export { FilterLabel, FilterItem, FilterItems } from './filter_bar'; diff --git a/src/plugins/unified_search/public/plugin.ts b/src/plugins/unified_search/public/plugin.ts index 05e22b035614d2..e853e6b77e8e12 100755 --- a/src/plugins/unified_search/public/plugin.ts +++ b/src/plugins/unified_search/public/plugin.ts @@ -11,7 +11,7 @@ import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { APPLY_FILTER_TRIGGER } from '@kbn/data-plugin/public'; import { UPDATE_FILTER_REFERENCES_TRIGGER, updateFilterReferencesTrigger } from './triggers'; import { ConfigSchema } from '../config'; -import { setIndexPatterns, setTheme, setOverlays, setAutocomplete } from './services'; +import { setIndexPatterns, setTheme, setOverlays } from './services'; import { AutocompleteService } from './autocomplete/autocomplete_service'; import { createSearchBar } from './search_bar'; import { createIndexPatternSelect } from './index_pattern_select'; @@ -70,7 +70,6 @@ export class UnifiedSearchPublicPlugin setOverlays(core.overlays); setIndexPatterns(dataViews); const autocompleteStart = this.autocomplete.start(); - setAutocomplete(autocompleteStart); const SearchBar = createSearchBar({ core, @@ -78,6 +77,9 @@ export class UnifiedSearchPublicPlugin storage: this.storage, usageCollection: this.usageCollection, isScreenshotMode: Boolean(screenshotMode?.isScreenshotMode()), + unifiedSearch: { + autocomplete: autocompleteStart, + }, }); uiActions.addTriggerAction( diff --git a/src/plugins/unified_search/public/query_string_input/filter_editor_wrapper.tsx b/src/plugins/unified_search/public/query_string_input/filter_editor_wrapper.tsx index dd106607353f2f..a0e36688a2f8b8 100644 --- a/src/plugins/unified_search/public/query_string_input/filter_editor_wrapper.tsx +++ b/src/plugins/unified_search/public/query_string_input/filter_editor_wrapper.tsx @@ -11,8 +11,8 @@ import { Filter, buildEmptyFilter } from '@kbn/es-query'; import { METRIC_TYPE } from '@kbn/analytics'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; -import { IDataPluginServices } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; +import type { IUnifiedSearchPluginServices } from '../types'; import { FILTER_EDITOR_WIDTH } from '../filter_bar/filter_item/filter_item'; import { FilterEditor } from '../filter_bar/filter_editor'; import { fetchIndexPatterns } from './fetch_index_patterns'; @@ -32,7 +32,7 @@ export const FilterEditorWrapper = React.memo(function FilterEditorWrapper({ closePopover, onFiltersUpdated, }: FilterEditorWrapperProps) { - const kibana = useKibana(); + const kibana = useKibana(); const { uiSettings, data, usageCollection, appName } = kibana.services; const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName); const [dataViews, setDataviews] = useState([]); diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx index 2b5dbf4999af14..4a921c3a1d1777 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx @@ -29,7 +29,8 @@ import { import { METRIC_TYPE } from '@kbn/analytics'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { KIBANA_USER_QUERY_LANGUAGE_KEY, UI_SETTINGS } from '@kbn/data-plugin/common'; -import type { IDataPluginServices, SavedQueryService, SavedQuery } from '@kbn/data-plugin/public'; +import type { SavedQueryService, SavedQuery } from '@kbn/data-plugin/public'; +import type { IUnifiedSearchPluginServices } from '../types'; import { fromUser } from './from_user'; import { QueryLanguageSwitcher } from './language_switcher'; import { FilterPanelOption } from '../types'; @@ -88,7 +89,7 @@ export function QueryBarMenuPanels({ onQueryChange, setRenderedComponent, }: QueryBarMenuPanelsProps) { - const kibana = useKibana(); + const kibana = useKibana(); const { appName, usageCollection, uiSettings, http, storage } = kibana.services; const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName); const cancelPendingListingRequest = useRef<() => void>(() => {}); diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.test.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.test.tsx index 1f879ebcae9a86..052e0ab7b32c82 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.test.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.test.tsx @@ -20,7 +20,6 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { I18nProvider } from '@kbn/i18n-react'; import { stubIndexPattern } from '@kbn/data-plugin/public/stubs'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; -import { setAutocomplete } from '../services'; import { unifiedSearchPluginMock } from '../mocks'; const startMock = coreMock.createStart(); @@ -96,6 +95,7 @@ function wrapQueryBarTopRowInContext(testProps: any) { const services = { ...startMock, + unifiedSearch: unifiedSearchPluginMock.createStartContract(), data: dataPluginMock.createStartContract(), appName: 'discover', storage: createMockStorage(), @@ -120,11 +120,6 @@ describe('QueryBarTopRowTopRow', () => { jest.clearAllMocks(); }); - beforeEach(() => { - const autocompleteStart = unifiedSearchPluginMock.createStartContract(); - setAutocomplete(autocompleteStart.autocomplete); - }); - it('Should render query and time picker', () => { const { getByText, getByTestId } = render( wrapQueryBarTopRowInContext({ diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx index ef6f09f679de21..c0848f630daa80 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx @@ -26,12 +26,13 @@ import { useIsWithinBreakpoints, EuiSuperUpdateButton, } from '@elastic/eui'; -import { IDataPluginServices, TimeHistoryContract, getQueryLog } from '@kbn/data-plugin/public'; +import { TimeHistoryContract, getQueryLog } from '@kbn/data-plugin/public'; import { i18n } from '@kbn/i18n'; import { DataView } from '@kbn/data-views-plugin/public'; import type { PersistedLog } from '@kbn/data-plugin/public'; import { useKibana, withKibana } from '@kbn/kibana-react-plugin/public'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; +import type { IUnifiedSearchPluginServices } from '../types'; import QueryStringInputUI from './query_string_input'; import { NoDataPopover } from './no_data_popover'; import { shallowEqual } from '../utils/shallow_equal'; @@ -164,7 +165,7 @@ export const QueryBarTopRow = React.memo( const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false); const [isQueryInputFocused, setIsQueryInputFocused] = useState(false); - const kibana = useKibana(); + const kibana = useKibana(); const { uiSettings, storage, appName } = kibana.services; const isQueryLangSelected = props.query && !isOfQueryType(props.query); diff --git a/src/plugins/unified_search/public/query_string_input/query_string_input.test.tsx b/src/plugins/unified_search/public/query_string_input/query_string_input.test.tsx index 7437bf5fd4ece9..41060aaecb3df1 100644 --- a/src/plugins/unified_search/public/query_string_input/query_string_input.test.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_string_input.test.tsx @@ -27,12 +27,9 @@ import { coreMock } from '@kbn/core/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { stubIndexPattern } from '@kbn/data-plugin/public/stubs'; import { KibanaContextProvider, withKibana } from '@kbn/kibana-react-plugin/public'; - -import { setAutocomplete } from '../services'; import { unifiedSearchPluginMock } from '../mocks'; jest.useFakeTimers(); - const startMock = coreMock.createStart(); const noop = () => { @@ -71,6 +68,7 @@ const QueryStringInput = withKibana(QueryStringInputUI); function wrapQueryStringInputInContext(testProps: any, storage?: any) { const services = { ...startMock, + unifiedSearch: unifiedSearchPluginMock.createStartContract(), data: dataPluginMock.createStartContract(), appName: testProps.appName || 'test', storage: storage || createMockStorage(), @@ -95,11 +93,6 @@ describe('QueryStringInput', () => { jest.clearAllMocks(); }); - beforeEach(() => { - const autocompleteStart = unifiedSearchPluginMock.createStartContract(); - setAutocomplete(autocompleteStart.autocomplete); - }); - it('Should render the given query', async () => { const { getByText } = render( wrapQueryStringInputInContext({ diff --git a/src/plugins/unified_search/public/query_string_input/query_string_input.tsx b/src/plugins/unified_search/public/query_string_input/query_string_input.tsx index d37c4bb72d40e9..84a12d8c632004 100644 --- a/src/plugins/unified_search/public/query_string_input/query_string_input.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_string_input.tsx @@ -30,7 +30,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { compact, debounce, isEmpty, isEqual, isFunction } from 'lodash'; import { Toast } from '@kbn/core/public'; import type { Query } from '@kbn/es-query'; -import { IDataPluginServices, getQueryLog } from '@kbn/data-plugin/public'; +import { getQueryLog } from '@kbn/data-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public'; import type { PersistedLog } from '@kbn/data-plugin/public'; import { getFieldSubtypeNested, KIBANA_USER_QUERY_LANGUAGE_KEY } from '@kbn/data-plugin/common'; @@ -41,11 +41,12 @@ import { fromUser } from './from_user'; import { fetchIndexPatterns } from './fetch_index_patterns'; import { QueryLanguageSwitcher } from './language_switcher'; import type { SuggestionsListSize } from '../typeahead/suggestions_component'; +import type { IUnifiedSearchPluginServices } from '../types'; import { SuggestionsComponent } from '../typeahead'; import { onRaf } from '../utils'; import { FilterButtonGroup } from '../filter_bar/filter_button_group/filter_button_group'; import { QuerySuggestion, QuerySuggestionTypes } from '../autocomplete'; -import { getTheme, getAutocomplete } from '../services'; +import { getTheme } from '../services'; import './query_string_input.scss'; export interface QueryStringInputProps { @@ -93,7 +94,7 @@ export interface QueryStringInputProps { } interface Props extends QueryStringInputProps { - kibana: KibanaReactContextValue; + kibana: KibanaReactContextValue; } interface State { @@ -202,7 +203,9 @@ export default class QueryStringInputUI extends PureComponent { const queryString = this.getQueryString(); const recentSearchSuggestions = this.getRecentSearchSuggestions(queryString); - const hasQuerySuggestions = getAutocomplete().hasQuerySuggestions(language); + const hasQuerySuggestions = await this.services.unifiedSearch.autocomplete.hasQuerySuggestions( + language + ); if ( !hasQuerySuggestions || @@ -223,7 +226,7 @@ export default class QueryStringInputUI extends PureComponent { if (this.abortController) this.abortController.abort(); this.abortController = new AbortController(); const suggestions = - (await getAutocomplete().getQuerySuggestions({ + (await this.services.unifiedSearch.autocomplete.getQuerySuggestions({ language, indexPatterns, query: queryString, @@ -378,7 +381,9 @@ export default class QueryStringInputUI extends PureComponent { } break; case KEY_CODES.ESC: - event.preventDefault(); + if (isSuggestionsVisible) { + event.preventDefault(); + } this.setState({ isSuggestionsVisible: false, index: null }); break; case KEY_CODES.TAB: diff --git a/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/index.tsx b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/index.tsx index 9b6bb6707ab49c..54343ec245efe8 100644 --- a/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/index.tsx +++ b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/index.tsx @@ -9,7 +9,6 @@ import React, { useRef, memo, useEffect, useState, useCallback } from 'react'; import classNames from 'classnames'; import { EsqlLang, monaco } from '@kbn/monaco'; -import { IDataPluginServices } from '@kbn/data-plugin/public'; import type { AggregateQuery } from '@kbn/es-query'; import { getAggregateQueryMode } from '@kbn/es-query'; import { useKibana } from '@kbn/kibana-react-plugin/public'; @@ -47,6 +46,7 @@ import { EditorFooter } from './editor_footer'; import { ResizableButton } from './resizable_button'; import './overwrite.scss'; +import { IUnifiedSearchPluginServices } from '../../types'; export interface TextBasedLanguagesEditorProps { query: AggregateQuery; @@ -106,7 +106,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ Array<{ startLineNumber: number; message: string }> >([]); const [documentationSections, setDocumentationSections] = useState(); - const kibana = useKibana(); + const kibana = useKibana(); const { uiSettings } = kibana.services; const styles = textBasedLanguagedEditorStyles( diff --git a/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.tsx b/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.tsx index 6200af754507a6..15f5295e5ee36e 100644 --- a/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.tsx +++ b/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.tsx @@ -27,9 +27,10 @@ import React, { useCallback, useEffect, useState, useRef } from 'react'; import { css } from '@emotion/react'; import { sortBy } from 'lodash'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { IDataPluginServices, SavedQuery, SavedQueryService } from '@kbn/data-plugin/public'; +import { SavedQuery, SavedQueryService } from '@kbn/data-plugin/public'; import type { SavedQueryAttributes } from '@kbn/data-plugin/common'; import './saved_query_management_list.scss'; +import type { IUnifiedSearchPluginServices } from '../types'; export interface SavedQueryManagementListProps { showSaveQuery?: boolean; @@ -120,7 +121,7 @@ export function SavedQueryManagementList({ onClose, hasFiltersOrQuery, }: SavedQueryManagementListProps) { - const kibana = useKibana(); + const kibana = useKibana(); const [savedQueries, setSavedQueries] = useState([] as SavedQuery[]); const [selectedSavedQuery, setSelectedSavedQuery] = useState(null as SavedQuery | null); const [toBeDeletedSavedQuery, setToBeDeletedSavedQuery] = useState(null as SavedQuery | null); diff --git a/src/plugins/unified_search/public/search_bar/create_search_bar.tsx b/src/plugins/unified_search/public/search_bar/create_search_bar.tsx index 39980f03e3cbe7..a4df4d0f1a76ec 100644 --- a/src/plugins/unified_search/public/search_bar/create_search_bar.tsx +++ b/src/plugins/unified_search/public/search_bar/create_search_bar.tsx @@ -21,13 +21,15 @@ import { useFilterManager } from './lib/use_filter_manager'; import { useTimefilter } from './lib/use_timefilter'; import { useSavedQuery } from './lib/use_saved_query'; import { useQueryStringManager } from './lib/use_query_string_manager'; +import { UnifiedSearchPublicPluginStart } from '../types'; interface StatefulSearchBarDeps { core: CoreStart; - data: Omit; + data: DataPublicPluginStart; storage: IStorageWrapper; usageCollection?: UsageCollectionSetup; isScreenshotMode?: boolean; + unifiedSearch: Omit; } export type StatefulSearchBarProps = @@ -127,6 +129,7 @@ export function createSearchBar({ data, usageCollection, isScreenshotMode = false, + unifiedSearch, }: StatefulSearchBarDeps) { // App name should come from the core application service. // Until it's available, we'll ask the user to provide it for the pre-wired component. @@ -179,6 +182,7 @@ export function createSearchBar({ data, storage, usageCollection, + unifiedSearch, ...core, }} > diff --git a/src/plugins/unified_search/public/search_bar/search_bar.tsx b/src/plugins/unified_search/public/search_bar/search_bar.tsx index ecfbb388081ba6..8a6396d9939733 100644 --- a/src/plugins/unified_search/public/search_bar/search_bar.tsx +++ b/src/plugins/unified_search/public/search_bar/search_bar.tsx @@ -19,9 +19,9 @@ import { Query, Filter, TimeRange, AggregateQuery, isOfQueryType } from '@kbn/es import { withKibana, KibanaReactContextValue } from '@kbn/kibana-react-plugin/public'; import type { TimeHistoryContract, SavedQuery } from '@kbn/data-plugin/public'; import type { SavedQueryAttributes } from '@kbn/data-plugin/common'; -import { IDataPluginServices } from '@kbn/data-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public'; +import type { IUnifiedSearchPluginServices } from '../types'; import { SavedQueryMeta, SaveQueryForm } from '../saved_query_form'; import { SavedQueryManagementList } from '../saved_query_management'; import { QueryBarMenu, QueryBarMenuProps } from '../query_string_input/query_bar_menu'; @@ -32,7 +32,7 @@ import type { SuggestionsListSize } from '../typeahead/suggestions_component'; import { searchBarStyles } from './search_bar.styles'; export interface SearchBarInjectedDeps { - kibana: KibanaReactContextValue; + kibana: KibanaReactContextValue; intl: InjectedIntl; timeHistory?: TimeHistoryContract; // Filter bar diff --git a/src/plugins/unified_search/public/services.ts b/src/plugins/unified_search/public/services.ts index f67801dd377300..4f8937baf8fdf0 100644 --- a/src/plugins/unified_search/public/services.ts +++ b/src/plugins/unified_search/public/services.ts @@ -9,7 +9,6 @@ import { ThemeServiceStart, OverlayStart } from '@kbn/core/public'; import { createGetterSetter } from '@kbn/kibana-utils-plugin/public'; import { DataViewsContract } from '@kbn/data-views-plugin/public'; -import { AutocompleteStart } from '.'; export const [getIndexPatterns, setIndexPatterns] = createGetterSetter('IndexPatterns'); @@ -17,6 +16,3 @@ export const [getIndexPatterns, setIndexPatterns] = export const [getTheme, setTheme] = createGetterSetter('Theme'); export const [getOverlays, setOverlays] = createGetterSetter('Overlays'); - -export const [getAutocomplete, setAutocomplete] = - createGetterSetter('Autocomplete'); diff --git a/src/plugins/unified_search/public/types.ts b/src/plugins/unified_search/public/types.ts index 3189e7cf32d08c..246fc87114db48 100755 --- a/src/plugins/unified_search/public/types.ts +++ b/src/plugins/unified_search/public/types.ts @@ -11,8 +11,10 @@ import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { ScreenshotModePluginStart } from '@kbn/screenshot-mode-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; +import { UsageCollectionSetup, UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; import { Query, AggregateQuery } from '@kbn/es-query'; +import { CoreStart } from '@kbn/core/public'; +import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { AutocompleteSetup, AutocompleteStart } from './autocomplete'; import type { IndexPatternSelectProps, StatefulSearchBarProps } from '.'; @@ -56,7 +58,7 @@ export interface UnifiedSearchPublicPluginStart { autocomplete: AutocompleteStart; /** * prewired UI components - * {@link DataPublicPluginStartUi} + * {@link UnifiedSearchPublicPluginStartUi} */ ui: UnifiedSearchPublicPluginStartUi; } @@ -70,3 +72,18 @@ export type FilterPanelOption = | 'negateFilter' | 'disableFilter' | 'deleteFilter'; + +export interface IUnifiedSearchPluginServices extends Partial { + unifiedSearch: { + autocomplete: AutocompleteStart; + }; + appName: string; + uiSettings: CoreStart['uiSettings']; + savedObjects: CoreStart['savedObjects']; + notifications: CoreStart['notifications']; + application: CoreStart['application']; + http: CoreStart['http']; + storage: IStorageWrapper; + data: DataPublicPluginStart; + usageCollection?: UsageCollectionStart; +} diff --git a/src/plugins/unified_search/public/utils/index.ts b/src/plugins/unified_search/public/utils/index.ts index 5dffd3798399db..395304c48a9145 100644 --- a/src/plugins/unified_search/public/utils/index.ts +++ b/src/plugins/unified_search/public/utils/index.ts @@ -8,3 +8,11 @@ export { onRaf } from './on_raf'; export { shallowEqual } from './shallow_equal'; + +export type { FilterItem } from './or_filter'; +export { + ConditionTypes, + isOrFilter, + getConditionalOperationType, + buildOrFilter, +} from './or_filter'; diff --git a/src/plugins/unified_search/public/utils/or_filter.ts b/src/plugins/unified_search/public/utils/or_filter.ts new file mode 100644 index 00000000000000..419e4f04d74ae0 --- /dev/null +++ b/src/plugins/unified_search/public/utils/or_filter.ts @@ -0,0 +1,47 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +// Methods from this file will be removed after they are moved to the package +import { buildEmptyFilter, Filter } from '@kbn/es-query'; + +export enum ConditionTypes { + OR = 'OR', + AND = 'AND', +} + +/** @internal **/ +export type FilterItem = Filter | FilterItem[]; + +/** to: @kbn/es-query **/ +export const isOrFilter = (filter: Filter) => Boolean(filter?.meta?.type === 'OR'); + +/** + * Defines a conditional operation type (AND/OR) from the filter otherwise returns undefined. + * @param {FilterItem} filter + */ +export const getConditionalOperationType = (filter: FilterItem) => { + if (Array.isArray(filter)) { + return ConditionTypes.AND; + } else if (isOrFilter(filter)) { + return ConditionTypes.OR; + } +}; + +/** to: @kbn/es-query **/ +export const buildOrFilter = (filters: FilterItem) => { + const filter = buildEmptyFilter(false); + + return { + ...filter, + meta: { + ...filter.meta, + type: 'OR', + params: filters, + }, + }; +}; diff --git a/src/plugins/usage_collection/server/usage_counters/saved_objects.ts b/src/plugins/usage_collection/server/usage_counters/saved_objects.ts index 2752696f24711f..701bbba404c497 100644 --- a/src/plugins/usage_collection/server/usage_counters/saved_objects.ts +++ b/src/plugins/usage_collection/server/usage_counters/saved_objects.ts @@ -9,7 +9,6 @@ import type { SavedObject, SavedObjectsRepository, - SavedObjectAttributes, SavedObjectsServiceSetup, } from '@kbn/core/server'; import moment from 'moment'; @@ -18,7 +17,7 @@ import type { CounterMetric } from './usage_counter'; /** * The attributes stored in the UsageCounters' SavedObjects */ -export interface UsageCountersSavedObjectAttributes extends SavedObjectAttributes { +export interface UsageCountersSavedObjectAttributes { /** The domain ID registered in the Usage Counter **/ domainId: string; /** The counter name **/ diff --git a/src/plugins/vis_types/timeseries/public/application/components/lib/create_field_formatter.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/create_field_formatter.ts index 0dc709e786c8bc..16ae8372301ed0 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/lib/create_field_formatter.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/create_field_formatter.ts @@ -33,6 +33,8 @@ export const createFieldFormatter = ( ? { id: 'date' } : fieldType === 'string' ? { id: 'string' } + : fieldType === 'boolean' + ? { id: 'boolean' } : { id: 'number' }; const fieldFormat = getFieldFormats().deserialize( diff --git a/test/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts index afb75a670881d4..841724347c0043 100644 --- a/test/accessibility/apps/discover.ts +++ b/test/accessibility/apps/discover.ts @@ -117,13 +117,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('a11y test for actions on a field', async () => { await PageObjects.discover.clickDocViewerTab(0); - await testSubjects.click('openFieldActionsButton-Cancelled'); + if (await testSubjects.exists('openFieldActionsButton-Cancelled')) { + await testSubjects.click('openFieldActionsButton-Cancelled'); + } else { + await testSubjects.existOrFail('fieldActionsGroup-Cancelled'); + } await a11y.testAppSnapshot(); }); it('a11y test for data-grid table with columns', async () => { await testSubjects.click('toggleColumnButton-Cancelled'); - await testSubjects.click('openFieldActionsButton-Carrier'); + if (await testSubjects.exists('openFieldActionsButton-Carrier')) { + await testSubjects.click('openFieldActionsButton-Carrier'); + } else { + await testSubjects.existOrFail('fieldActionsGroup-Carrier'); + } await testSubjects.click('toggleColumnButton-Carrier'); await testSubjects.click('euiFlyoutCloseButton'); await toasts.dismissAllToasts(); diff --git a/test/examples/search/warnings.ts b/test/examples/search/warnings.ts index 05179aa926f86c..fc1949549d66e8 100644 --- a/test/examples/search/warnings.ts +++ b/test/examples/search/warnings.ts @@ -6,15 +6,19 @@ * Side Public License, v 1. */ +import type { estypes } from '@elastic/elasticsearch'; import expect from '@kbn/expect'; import { asyncForEach } from '@kbn/std'; -import { FtrProviderContext } from '../../functional/ftr_provider_context'; +import assert from 'assert'; +import type { FtrProviderContext } from '../../functional/ftr_provider_context'; +import type { WebElementWrapper } from '../../functional/services/lib/web_element_wrapper'; // eslint-disable-next-line import/no-default-export export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'timePicker']); const testSubjects = getService('testSubjects'); const find = getService('find'); + const retry = getService('retry'); const es = getService('es'); const log = getService('log'); const indexPatterns = getService('indexPatterns'); @@ -22,8 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const esArchiver = getService('esArchiver'); - // Failing: See https://github.com/elastic/kibana/issues/139879 - describe.skip('handling warnings with search source fetch', function () { + describe('handling warnings with search source fetch', function () { const dataViewTitle = 'sample-01,sample-01-rollup'; const fromTime = 'Jun 17, 2022 @ 00:00:00.000'; const toTime = 'Jun 23, 2022 @ 00:00:00.000'; @@ -51,10 +54,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await es.indices.addBlock({ index: testIndex, block: 'write' }); try { log.info(`rolling up ${testIndex} index...`); - await es.rollup.rollup({ - index: testIndex, - rollup_index: testRollupIndex, - config: { fixed_interval: '1h' }, + // es client currently does not have method for downsample + await es.transport.request({ + method: 'POST', + path: '/sample-01/_downsample/sample-01-rollup', + body: { fixed_interval: '1h' }, }); } catch (err) { log.info(`ignoring resource_already_exists_exception...`); @@ -76,6 +80,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { defaultIndex: '0ae0bc7a-e4ca-405c-ab67-f2b5913f2a51', 'timepicker:timeDefaults': '{ "from": "now-1y", "to": "now" }', }); + + await PageObjects.common.navigateToApp('searchExamples'); + }); + + beforeEach(async () => { + await comboBox.setCustom('dataViewSelector', dataViewTitle); + await comboBox.set('searchMetricField', testRollupField); + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); }); after(async () => { @@ -84,23 +96,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.replace({}); }); - beforeEach(async () => { - // reload the page to clear toasts from previous test - - await PageObjects.common.navigateToApp('searchExamples'); - - await comboBox.setCustom('dataViewSelector', dataViewTitle); - await comboBox.set('searchMetricField', testRollupField); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + afterEach(async () => { + await PageObjects.common.clearAllToasts(); }); it('shows shard failure warning notifications by default', async () => { await testSubjects.click('searchSourceWithOther'); + // wait for response - toasts appear before the response is rendered + let response: estypes.SearchResponse | undefined; + await retry.try(async () => { + response = await getTestJson('responseTab', 'responseCodeBlock'); + expect(response).not.to.eql({}); + }); + // toasts const toasts = await find.allByCssSelector(toastsSelector); - expect(toasts.length).to.be(3); - const expects = ['2 of 4 shards failed', '2 of 4 shards failed', 'Query result']; // BUG: there are 2 shards failed toast notifications + expect(toasts.length).to.be(2); + const expects = ['2 of 4 shards failed', 'Query result']; await asyncForEach(toasts, async (t, index) => { expect(await t.getVisibleText()).to.eql(expects[index]); }); @@ -119,12 +132,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const responseBlock = await testSubjects.find('shardsFailedModalResponseBlock'); expect(await responseBlock.getVisibleText()).to.contain(shardFailureReason); - // close things await testSubjects.click('closeShardFailureModal'); - await PageObjects.common.clearAllToasts(); // response tab - const response = await getTestJson('responseTab', 'responseCodeBlock'); + assert(response && response._shards.failures); expect(response._shards.total).to.be(4); expect(response._shards.successful).to.be(2); expect(response._shards.skipped).to.be(0); @@ -142,9 +153,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('able to handle shard failure warnings and prevent default notifications', async () => { await testSubjects.click('searchSourceWithoutOther'); - // toasts - const toasts = await find.allByCssSelector(toastsSelector); - expect(toasts.length).to.be(2); + // wait for toasts - toasts appear after the response is rendered + let toasts: WebElementWrapper[] = []; + await retry.try(async () => { + toasts = await find.allByCssSelector(toastsSelector); + expect(toasts.length).to.be(2); + }); const expects = ['2 of 4 shards failed', 'Query result']; await asyncForEach(toasts, async (t, index) => { expect(await t.getVisibleText()).to.eql(expects[index]); @@ -164,9 +178,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const responseBlock = await testSubjects.find('shardsFailedModalResponseBlock'); expect(await responseBlock.getVisibleText()).to.contain(shardFailureReason); - // close things await testSubjects.click('closeShardFailureModal'); - await PageObjects.common.clearAllToasts(); // response tab const response = await getTestJson('responseTab', 'responseCodeBlock'); diff --git a/test/functional/apps/context/_filters.ts b/test/functional/apps/context/_filters.ts index 8c77d4fd013c19..f9e95080c92e4b 100644 --- a/test/functional/apps/context/_filters.ts +++ b/test/functional/apps/context/_filters.ts @@ -18,7 +18,6 @@ const TEST_COLUMN_NAMES = ['extension', 'geo.src']; export default function ({ getService, getPageObjects }: FtrProviderContext) { const dataGrid = getService('dataGrid'); const filterBar = getService('filterBar'); - const testSubjects = getService('testSubjects'); const retry = getService('retry'); const browser = getService('browser'); @@ -34,12 +33,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('inclusive filter should be addable via expanded data grid rows', async function () { await retry.waitFor(`filter ${TEST_ANCHOR_FILTER_FIELD} in filterbar`, async () => { await dataGrid.clickRowToggle({ isAnchorRow: true, renderMoreRows: true }); - await testSubjects.click(`openFieldActionsButton-${TEST_ANCHOR_FILTER_FIELD}`); - await testSubjects.click(`addFilterForValueButton-${TEST_ANCHOR_FILTER_FIELD}`); + await dataGrid.clickFieldActionInFlyout( + TEST_ANCHOR_FILTER_FIELD, + 'addFilterForValueButton' + ); await PageObjects.context.waitUntilContextLoadingHasFinished(); return await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, true); }); + + await dataGrid.closeFlyout(); + await retry.waitFor(`filter matching docs in data grid`, async () => { const fields = await dataGrid.getFields(); return fields @@ -71,8 +75,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('filter for presence should be addable via expanded data grid rows', async function () { await retry.waitFor('an exists filter in the filterbar', async () => { await dataGrid.clickRowToggle({ isAnchorRow: true, renderMoreRows: true }); - await testSubjects.click(`openFieldActionsButton-${TEST_ANCHOR_FILTER_FIELD}`); - await testSubjects.click(`addExistsFilterButton-${TEST_ANCHOR_FILTER_FIELD}`); + await dataGrid.clickFieldActionInFlyout(TEST_ANCHOR_FILTER_FIELD, 'addExistsFilterButton'); await PageObjects.context.waitUntilContextLoadingHasFinished(); return await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, 'exists', true); }); diff --git a/test/functional/apps/dashboard/group2/dashboard_filter_bar.ts b/test/functional/apps/dashboard/group2/dashboard_filter_bar.ts index f2476f7516611c..1244e179f7f6a1 100644 --- a/test/functional/apps/dashboard/group2/dashboard_filter_bar.ts +++ b/test/functional/apps/dashboard/group2/dashboard_filter_bar.ts @@ -224,8 +224,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.loadSavedDashboard('dashboard with bad filters'); }); - it('filter with non-existent index pattern renders in error mode', async function () { - const hasBadFieldFilter = await filterBar.hasFilter('name', 'error', false); + it('filter with non-existent index pattern renders if it matches a field', async function () { + const hasBadFieldFilter = await filterBar.hasFilter('name', 'moo', false); expect(hasBadFieldFilter).to.be(true); }); diff --git a/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts b/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts index 08a0296ad8c083..bd47c072e77359 100644 --- a/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts +++ b/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts @@ -77,5 +77,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dataGrid.checkCurrentRowsPerPageToBe(10); }); + + it('should render duplicate saved search embeddables', async () => { + await PageObjects.dashboard.switchToEditMode(); + await addSearchEmbeddableToDashboard(); + const [firstGridCell, secondGridCell] = await dataGrid.getAllCellElements(); + const firstGridCellContent = await firstGridCell.getVisibleText(); + const secondGridCellContent = await secondGridCell.getVisibleText(); + + expect(firstGridCellContent).to.be.equal(secondGridCellContent); + }); }); } diff --git a/test/functional/apps/discover/group2/_data_grid_doc_navigation.ts b/test/functional/apps/discover/group2/_data_grid_doc_navigation.ts index 6ea883f7a560dc..2041d5fe500fc7 100644 --- a/test/functional/apps/discover/group2/_data_grid_doc_navigation.ts +++ b/test/functional/apps/discover/group2/_data_grid_doc_navigation.ts @@ -60,9 +60,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.waitUntilSearchingHasFinished(); await dataGrid.clickRowToggle({ rowIndex: 0 }); - - await testSubjects.click('openFieldActionsButton-@timestamp'); - await testSubjects.click('addExistsFilterButton-@timestamp'); + await dataGrid.clickFieldActionInFlyout('@timestamp', 'addExistsFilterButton'); const hasExistsFilter = await filterBar.hasFilter('@timestamp', 'exists', true, false, false); expect(hasExistsFilter).to.be(true); diff --git a/test/functional/apps/discover/group2/_data_grid_doc_table.ts b/test/functional/apps/discover/group2/_data_grid_doc_table.ts index c2f55847e7d1ee..a90932595d42a6 100644 --- a/test/functional/apps/discover/group2/_data_grid_doc_table.ts +++ b/test/functional/apps/discover/group2/_data_grid_doc_table.ts @@ -197,8 +197,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // add columns const fields = ['_id', '_index', 'agent']; for (const field of fields) { - await testSubjects.click(`openFieldActionsButton-${field}`); - await testSubjects.click(`toggleColumnButton-${field}`); + await dataGrid.clickFieldActionInFlyout(field, 'toggleColumnButton'); } const headerWithFields = await dataGrid.getHeaderFields(); @@ -206,8 +205,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // remove columns for (const field of fields) { - await testSubjects.click(`openFieldActionsButton-${field}`); - await testSubjects.click(`toggleColumnButton-${field}`); + await dataGrid.clickFieldActionInFlyout(field, 'toggleColumnButton'); } const headerWithoutFields = await dataGrid.getHeaderFields(); diff --git a/test/functional/apps/management/_runtime_fields_composite.ts b/test/functional/apps/management/_runtime_fields_composite.ts new file mode 100644 index 00000000000000..47ea33e443d225 --- /dev/null +++ b/test/functional/apps/management/_runtime_fields_composite.ts @@ -0,0 +1,90 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); + const log = getService('log'); + const browser = getService('browser'); + const retry = getService('retry'); + const PageObjects = getPageObjects(['settings']); + const testSubjects = getService('testSubjects'); + + describe('runtime fields', function () { + this.tags(['skipFirefox']); + + before(async function () { + await browser.setWindowSize(1200, 800); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.uiSettings.replace({}); + }); + + after(async function afterAll() { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + }); + + describe('create composite runtime field', function describeIndexTests() { + // Starting with '@' to sort toward start of field list + const fieldName = '@composite_test'; + + it('should create runtime field', async function () { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndexPatterns(); + await PageObjects.settings.clickIndexPatternLogstash(); + const startingCount = parseInt(await PageObjects.settings.getFieldsTabCount(), 10); + await log.debug('add runtime field'); + await PageObjects.settings.addCompositeRuntimeField( + fieldName, + "emit('a','hello world')", + false, + 1 + ); + + await log.debug('check that field preview is rendered'); + expect(await testSubjects.exists('fieldPreviewItem', { timeout: 1500 })).to.be(true); + + await PageObjects.settings.clickSaveField(); + + await retry.try(async function () { + expect(parseInt(await PageObjects.settings.getFieldsTabCount(), 10)).to.be( + startingCount + 1 + ); + }); + }); + + it('should modify runtime field', async function () { + const startingCount = parseInt(await PageObjects.settings.getFieldsTabCount(), 10); + await PageObjects.settings.filterField(fieldName); + await testSubjects.click('editFieldFormat'); + // wait for subfields to render + await testSubjects.find(`typeField_0`); + await new Promise((e) => setTimeout(e, 2000)); + await PageObjects.settings.setCompositeScript("emit('a',6);emit('b',10);"); + + // wait for subfields to render + await testSubjects.find(`typeField_1`); + await new Promise((e) => setTimeout(e, 500)); + + await PageObjects.settings.clickSaveField(); + await testSubjects.click('clearSearchButton'); + await retry.try(async function () { + expect(parseInt(await PageObjects.settings.getFieldsTabCount(), 10)).to.be( + startingCount + 1 + ); + }); + }); + + it('should delete runtime field', async function () { + await testSubjects.click('deleteField'); + await PageObjects.settings.confirmDelete(); + }); + }); + }); +} diff --git a/test/functional/apps/management/index.ts b/test/functional/apps/management/index.ts index ded50870d79567..9b905b5a010743 100644 --- a/test/functional/apps/management/index.ts +++ b/test/functional/apps/management/index.ts @@ -32,6 +32,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_scripted_fields')); loadTestFile(require.resolve('./_scripted_fields_classic_table')); loadTestFile(require.resolve('./_runtime_fields')); + loadTestFile(require.resolve('./_runtime_fields_composite')); loadTestFile(require.resolve('./_field_formatter')); loadTestFile(require.resolve('./_legacy_url_redirect')); loadTestFile(require.resolve('./_exclude_index_pattern')); diff --git a/test/functional/apps/visualize/group4/_tsvb_chart.ts b/test/functional/apps/visualize/group4/_tsvb_chart.ts index 013c0473a59b97..b71458c5c55273 100644 --- a/test/functional/apps/visualize/group4/_tsvb_chart.ts +++ b/test/functional/apps/visualize/group4/_tsvb_chart.ts @@ -18,17 +18,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const security = getService('security'); const kibanaServer = getService('kibanaServer'); - const { timePicker, visChart, visualBuilder, visualize, settings } = getPageObjects([ - 'timePicker', + const { visChart, visualBuilder, visualize, settings, common } = getPageObjects([ 'visChart', 'visualBuilder', 'visualize', 'settings', + 'common', ]); + const from = 'Sep 19, 2015 @ 06:31:44.000'; + const to = 'Sep 22, 2015 @ 18:31:44.000'; + describe('visual builder', function describeIndexTests() { before(async () => { await visualize.initTests(); + await common.setTime({ from, to }); }); beforeEach(async () => { @@ -36,6 +40,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ['kibana_admin', 'test_logstash_reader', 'kibana_sample_admin'], { skipBrowserRefresh: true } ); + await visualize.navigateToNewVisualization(); await visualize.clickVisualBuilder(); await visualBuilder.checkVisualBuilderIsPresent(); @@ -398,10 +403,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await visualBuilder.setMetricsDataTimerangeMode('Last value'); await visualBuilder.setDropLastBucket(true); await visualBuilder.clickDataTab('metric'); - await timePicker.setAbsoluteRange( - 'Sep 19, 2015 @ 06:31:44.000', - 'Sep 22, 2015 @ 18:31:44.000' - ); }); const switchIndexTest = async (useKibanaIndexes: boolean) => { @@ -435,10 +436,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await visualBuilder.clickPanelOptions('metric'); await visualBuilder.setMetricsDataTimerangeMode('Last value'); await visualBuilder.setDropLastBucket(true); - await timePicker.setAbsoluteRange( - 'Sep 19, 2015 @ 06:31:44.000', - 'Sep 22, 2015 @ 18:31:44.000' - ); }); it('should be able to switch to gte interval (>=2d)', async () => { diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index ab47fb31fea54d..b0f2efea409933 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -700,6 +700,25 @@ export class SettingsPageObject extends FtrService { if (script) { await this.setFieldScript(script); } + + if (doSaveField) { + await this.clickSaveField(); + } + } + + async addCompositeRuntimeField( + name: string, + script: string, + doSaveField = true, + subfieldCount = 0 + ) { + await this.clickAddField(); + await this.setFieldName(name); + await this.setFieldTypeComposite(); + await this.setCompositeScript(script); + if (subfieldCount > 0) { + await this.testSubjects.find(`typeField_${subfieldCount - 1}`); + } if (doSaveField) { await this.clickSaveField(); } @@ -765,6 +784,12 @@ export class SettingsPageObject extends FtrService { await this.testSubjects.setValue('typeField', type); } + async setFieldTypeComposite() { + this.log.debug('set type = Composite'); + await this.testSubjects.setValue('typeField', 'Composite'); + await this.browser.pressKeys(this.browser.keys.RETURN); + } + async setFieldScript(script: string) { this.log.debug('set script = ' + script); await this.toggleRow('valueRow'); @@ -772,6 +797,12 @@ export class SettingsPageObject extends FtrService { await this.monacoEditor.setCodeEditorValue(script); } + async setCompositeScript(script: string) { + this.log.debug('set composite script = ' + script); + await this.monacoEditor.waitCodeEditorReady('scriptFieldRow'); + await this.monacoEditor.setCodeEditorValue(script); + } + async clickAddScriptedField() { this.log.debug('click Add Scripted Field'); await this.testSubjects.click('addScriptedFieldLink'); diff --git a/test/functional/services/common/find.ts b/test/functional/services/common/find.ts index da12279a3ffa10..ec2d5385a0a432 100644 --- a/test/functional/services/common/find.ts +++ b/test/functional/services/common/find.ts @@ -10,7 +10,6 @@ import { WebDriver, WebElement, By, until } from 'selenium-webdriver'; import { Browsers } from '../remote/browsers'; import { FtrService, FtrProviderContext } from '../../ftr_provider_context'; -import { retryOnStale } from './retry_on_stale'; import { WebElementWrapper } from '../lib/web_element_wrapper'; import { TimeoutOpt } from './types'; @@ -18,6 +17,7 @@ export class FindService extends FtrService { private readonly log = this.ctx.getService('log'); private readonly config = this.ctx.getService('config'); private readonly retry = this.ctx.getService('retry'); + private readonly retryOnStale = this.ctx.getService('retryOnStale'); private readonly WAIT_FOR_EXISTS_TIME = this.config.get('timeouts.waitForExists'); private readonly POLLING_TIME = 500; @@ -290,7 +290,7 @@ export class FindService extends FtrService { public async clickByCssSelectorWhenNotDisabled(selector: string, opts?: TimeoutOpt) { const timeout = opts?.timeout ?? this.defaultFindTimeout; - await retryOnStale(this.log, async () => { + await this.retryOnStale(async () => { this.log.debug(`Find.clickByCssSelectorWhenNotDisabled(${selector}, timeout=${timeout})`); const element = await this.byCssSelector(selector); diff --git a/test/functional/services/common/index.ts b/test/functional/services/common/index.ts index b7b8c67a4280d9..54c9e5a1ee54cc 100644 --- a/test/functional/services/common/index.ts +++ b/test/functional/services/common/index.ts @@ -14,3 +14,4 @@ export { PngService } from './png'; export { ScreenshotsService } from './screenshots'; export { SnapshotsService } from './snapshots'; export { TestSubjects } from './test_subjects'; +export { RetryOnStaleProvider } from './retry_on_stale'; diff --git a/test/functional/services/common/retry_on_stale.ts b/test/functional/services/common/retry_on_stale.ts index a240e8031cd685..4a190266458ec6 100644 --- a/test/functional/services/common/retry_on_stale.ts +++ b/test/functional/services/common/retry_on_stale.ts @@ -6,30 +6,44 @@ * Side Public License, v 1. */ -import { ToolingLog } from '@kbn/tooling-log'; +import { FtrProviderContext } from '../../ftr_provider_context'; const MAX_ATTEMPTS = 10; const isObj = (v: unknown): v is Record => typeof v === 'object' && v !== null; const errMsg = (err: unknown) => (isObj(err) && typeof err.message === 'string' ? err.message : ''); -export async function retryOnStale(log: ToolingLog, fn: () => Promise): Promise { - let attempt = 0; - while (true) { - attempt += 1; - try { - return await fn(); - } catch (error) { - if (errMsg(error).includes('stale element reference')) { - if (attempt >= MAX_ATTEMPTS) { - throw new Error(`retryOnStale ran out of attempts after ${attempt} tries`); +export function RetryOnStaleProvider({ getService }: FtrProviderContext) { + const log = getService('log'); + + async function retryOnStale(fn: () => Promise): Promise { + let attempt = 0; + while (true) { + attempt += 1; + try { + return await fn(); + } catch (error) { + if (errMsg(error).includes('stale element reference')) { + if (attempt >= MAX_ATTEMPTS) { + throw new Error(`retryOnStale ran out of attempts after ${attempt} tries`); + } + + log.warning('stale element exception caught, retrying'); + continue; } - log.warning('stale element exception caught, retrying'); - continue; + throw error; } - - throw error; } } + + retryOnStale.wrap = (fn: (...args: Args) => Promise) => { + return async (...args: Args) => { + return await retryOnStale(async () => { + return await fn(...args); + }); + }; + }; + + return retryOnStale; } diff --git a/test/functional/services/data_grid.ts b/test/functional/services/data_grid.ts index fbd4310489fef2..68b2553478df7e 100644 --- a/test/functional/services/data_grid.ts +++ b/test/functional/services/data_grid.ts @@ -80,15 +80,24 @@ export class DataGridService extends FtrService { .map((cell) => $(cell).text()); } + private getCellElementSelector(rowIndex: number = 0, columnIndex: number = 0) { + return `[data-test-subj="euiDataGridBody"] [data-test-subj="dataGridRowCell"][data-gridcell-column-index="${columnIndex}"][data-gridcell-row-index="${rowIndex}"]`; + } + /** * Returns a grid cell element by row & column indexes. * @param rowIndex data row index starting from 0 (0 means 1st row) * @param columnIndex column index starting from 0 (0 means 1st column) */ public async getCellElement(rowIndex: number = 0, columnIndex: number = 0) { - return await this.find.byCssSelector( - `[data-test-subj="euiDataGridBody"] [data-test-subj="dataGridRowCell"][data-gridcell-column-index="${columnIndex}"][data-gridcell-row-index="${rowIndex}"]` - ); + return await this.find.byCssSelector(this.getCellElementSelector(rowIndex, columnIndex)); + } + + /** + * The same as getCellElement, but useful when multiple data grids are on the page. + */ + public async getAllCellElements(rowIndex: number = 0, columnIndex: number = 0) { + return await this.find.allByCssSelector(this.getCellElementSelector(rowIndex, columnIndex)); } public async getDocCount(): Promise { @@ -312,6 +321,17 @@ export class DataGridService extends FtrService { return await tableDocViewRow.findByTestSubject(`~removeInclusiveFilterButton`); } + public async clickFieldActionInFlyout(fieldName: string, actionName: string): Promise { + const openPopoverButtonSelector = `openFieldActionsButton-${fieldName}`; + const inlineButtonsGroupSelector = `fieldActionsGroup-${fieldName}`; + if (await this.testSubjects.exists(openPopoverButtonSelector)) { + await this.testSubjects.click(openPopoverButtonSelector); + } else { + await this.testSubjects.existOrFail(inlineButtonsGroupSelector); + } + await this.testSubjects.click(`${actionName}-${fieldName}`); + } + public async removeInclusiveFilter( detailsRow: WebElementWrapper, fieldName: string diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index 31d31e6424177e..e2186ddefa5faa 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -17,6 +17,7 @@ import { ScreenshotsService, SnapshotsService, TestSubjects, + RetryOnStaleProvider, } from './common'; import { ComboBoxService } from './combo_box'; import { @@ -88,4 +89,5 @@ export const services = { managementMenu: ManagementMenuService, monacoEditor: MonacoEditorService, menuToggle: MenuToggleService, + retryOnStale: RetryOnStaleProvider, }; diff --git a/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts b/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts index ce8119cfd0acc9..4c3abebc8143f1 100644 --- a/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts +++ b/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts @@ -16,7 +16,7 @@ export class SavedObjectExportTransformsPlugin implements Plugin { // example of a SO type that will mutates its properties // during the export transform - savedObjects.registerType({ + savedObjects.registerType<{ title: string; enabled: boolean }>({ name: 'test-export-transform', hidden: false, namespaceType: 'single', @@ -46,7 +46,7 @@ export class SavedObjectExportTransformsPlugin implements Plugin { // example of a SO type that will add additional objects // to the export during the export transform - savedObjects.registerType({ + savedObjects.registerType<{ title: string }>({ name: 'test-export-add', hidden: false, namespaceType: 'single', @@ -74,7 +74,7 @@ export class SavedObjectExportTransformsPlugin implements Plugin { // dependency of `test_export_transform_2` that will be included // when exporting them - savedObjects.registerType({ + savedObjects.registerType<{ title: string }>({ name: 'test-export-add-dep', hidden: false, namespaceType: 'single', @@ -91,7 +91,7 @@ export class SavedObjectExportTransformsPlugin implements Plugin { }); // example of a SO type that will throw an object-transform-error - savedObjects.registerType({ + savedObjects.registerType<{ title: string }>({ name: 'test-export-transform-error', hidden: false, namespaceType: 'single', @@ -111,7 +111,7 @@ export class SavedObjectExportTransformsPlugin implements Plugin { }); // example of a SO type that will throw an invalid-transform-error - savedObjects.registerType({ + savedObjects.registerType<{ title: string }>({ name: 'test-export-invalid-transform', hidden: false, namespaceType: 'single', @@ -134,7 +134,7 @@ export class SavedObjectExportTransformsPlugin implements Plugin { }); // example of a SO type that is exportable while being hidden - savedObjects.registerType({ + savedObjects.registerType<{ title: string; enabled: boolean }>({ name: 'test-actions-export-hidden', hidden: true, namespaceType: 'single', diff --git a/test/plugin_functional/plugins/saved_object_import_warnings/server/plugin.ts b/test/plugin_functional/plugins/saved_object_import_warnings/server/plugin.ts index 364f094ccf7cb1..f2d99539df19fd 100644 --- a/test/plugin_functional/plugins/saved_object_import_warnings/server/plugin.ts +++ b/test/plugin_functional/plugins/saved_object_import_warnings/server/plugin.ts @@ -10,7 +10,7 @@ import { Plugin, CoreSetup } from '@kbn/core/server'; export class SavedObjectImportWarningsPlugin implements Plugin { public setup({ savedObjects }: CoreSetup, deps: {}) { - savedObjects.registerType({ + savedObjects.registerType<{ title: string }>({ name: 'test_import_warning_1', hidden: false, namespaceType: 'single', @@ -31,7 +31,7 @@ export class SavedObjectImportWarningsPlugin implements Plugin { }, }); - savedObjects.registerType({ + savedObjects.registerType<{ title: string }>({ name: 'test_import_warning_2', hidden: false, namespaceType: 'single', diff --git a/test/visual_regression/config.ts b/test/visual_regression/config.ts deleted file mode 100644 index 294848246e7c85..00000000000000 --- a/test/visual_regression/config.ts +++ /dev/null @@ -1,30 +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 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 or the Server - * Side Public License, v 1. - */ - -import { FtrConfigProviderContext } from '@kbn/test'; -import { services } from './services'; - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); - - return { - ...functionalConfig.getAll(), - - testFiles: [ - require.resolve('./tests/console_app'), - require.resolve('./tests/discover'), - require.resolve('./tests/vega'), - ], - - services, - - junit: { - reportName: 'Kibana Visual Regression Tests', - }, - }; -} diff --git a/test/visual_regression/services/visual_testing/take_percy_snapshot.js b/test/visual_regression/services/visual_testing/take_percy_snapshot.js deleted file mode 100644 index 5325765c8d06bc..00000000000000 --- a/test/visual_regression/services/visual_testing/take_percy_snapshot.js +++ /dev/null @@ -1,102 +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 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 or the Server - * Side Public License, v 1. - */ - -import { readFileSync } from 'fs'; -import { agentJsFilename } from '@percy/agent/dist/utils/sdk-utils'; - -export function takePercySnapshot(show, hide) { - if (!window.PercyAgent) { - return false; - } - - // add percy styles to hide/show specific elements - const styleElement = document.createElement('style'); - styleElement.appendChild( - document.createTextNode(` - .hideInPercy { - visibility: hidden; - - .showInPercy { - visibility: visible; - } - } - - .showInPercy { - visibility: visible; - - .hideInPercy { - visibility: hidden; - } - } - `) - ); - document.head.appendChild(styleElement); - - const add = (selectors, className) => { - for (const selector of selectors) { - for (const element of document.querySelectorAll(selector)) { - element.classList.add(className); - } - } - }; - - const remove = (selectors, className) => { - for (const selector of selectors) { - for (const element of document.querySelectorAll(selector)) { - element.classList.remove(className); - } - } - }; - - // set Percy visibility on elements - add(hide, 'hideInPercy'); - if (show.length > 0) { - // hide the body by default - add(['body'], 'hideInPercy'); - add(show, 'showInPercy'); - } - - // convert canvas elements into static images - const replacements = []; - for (const canvas of document.querySelectorAll('canvas')) { - const image = document.createElement('img'); - image.classList.value = canvas.classList.value; - image.src = canvas.toDataURL(); - image.style.cssText = window.getComputedStyle(canvas).cssText; - canvas.parentElement.replaceChild(image, canvas); - replacements.push({ canvas, image }); - } - - try { - const agent = new window.PercyAgent({ - handleAgentCommunication: false, - }); - - // cache the dom snapshot containing the images - return agent.snapshot(document, { - widths: [document.documentElement.clientWidth], - }); - } finally { - // restore replaced canvases - for (const { image, canvas } of replacements) { - image.parentElement.replaceChild(canvas, image); - } - - // restore element visibility - document.head.removeChild(styleElement); - remove(['body'], 'hideInPercy'); - remove(show, 'showInPercy'); - remove(hide, 'hideInPercy'); - } -} - -export const takePercySnapshotWithAgent = ` - ${readFileSync(agentJsFilename(), 'utf8')} - - return (${takePercySnapshot.toString()}).apply(null, arguments); -`; diff --git a/test/visual_regression/services/visual_testing/visual_testing.ts b/test/visual_regression/services/visual_testing/visual_testing.ts deleted file mode 100644 index 59c601e6a2b6e6..00000000000000 --- a/test/visual_regression/services/visual_testing/visual_testing.ts +++ /dev/null @@ -1,114 +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 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 or the Server - * Side Public License, v 1. - */ - -import { postSnapshot } from '@percy/agent/dist/utils/sdk-utils'; -import testSubjSelector from '@kbn/test-subj-selector'; -import { Test } from '@kbn/test'; -import { kibanaPackageJson as pkg } from '@kbn/utils'; -import { FtrService, FtrProviderContext } from '../../ftr_provider_context'; - -// @ts-ignore internal js that is passed to the browser as is -import { takePercySnapshot, takePercySnapshotWithAgent } from './take_percy_snapshot'; - -export const DEFAULT_OPTIONS = { - widths: [1200], -}; - -export interface SnapshotOptions { - /** - * name to append to visual test name - */ - name?: string; - /** - * test subject selectiors to __show__ in screenshot - */ - show?: string[]; - /** - * test subject selectiors to __hide__ in screenshot - */ - hide?: string[]; -} - -const statsCache = new WeakMap(); - -function getStats(test: Test) { - if (!statsCache.has(test)) { - statsCache.set(test, { - snapshotCount: 0, - }); - } - - return statsCache.get(test)!; -} - -export class VisualTestingService extends FtrService { - private readonly browser = this.ctx.getService('browser'); - private readonly log = this.ctx.getService('log'); - - private currentTest: Test | undefined; - - constructor(ctx: FtrProviderContext) { - super(ctx); - - this.ctx.getService('lifecycle').beforeEachTest.add((test) => { - this.currentTest = test; - }); - } - - public async snapshot(options: SnapshotOptions = {}) { - if (process.env.DISABLE_VISUAL_TESTING) { - this.log.warning( - 'Capturing of percy snapshots disabled, would normally capture a snapshot here!' - ); - return; - } - - this.log.debug('Capturing percy snapshot'); - - if (!this.currentTest) { - throw new Error('unable to determine current test'); - } - - const [domSnapshot, url] = await Promise.all([ - this.getSnapshot(options.show, options.hide), - this.browser.getCurrentUrl(), - ]); - const stats = getStats(this.currentTest); - stats.snapshotCount += 1; - - const { name } = options; - const success = await postSnapshot({ - name: `${this.currentTest.fullTitle()} [${name ? name : stats.snapshotCount}]`, - url, - domSnapshot, - clientInfo: `kibana-ftr:${pkg.version}`, - ...DEFAULT_OPTIONS, - }); - - if (!success) { - throw new Error('Percy snapshot failed'); - } - } - - private async getSnapshot(show: string[] = [], hide: string[] = []) { - const showSelectors = show.map(testSubjSelector); - const hideSelectors = hide.map(testSubjSelector); - const snapshot = await this.browser.execute<[string[], string[]], string | false>( - takePercySnapshot, - showSelectors, - hideSelectors - ); - return snapshot !== false - ? snapshot - : await this.browser.execute<[string[], string[]], string>( - takePercySnapshotWithAgent, - showSelectors, - hideSelectors - ); - } -} diff --git a/test/visual_regression/tests/console_app.ts b/test/visual_regression/tests/console_app.ts deleted file mode 100644 index 2c2351b76ad4f9..00000000000000 --- a/test/visual_regression/tests/console_app.ts +++ /dev/null @@ -1,71 +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 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 or the Server - * Side Public License, v 1. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; - -const DEFAULT_REQUEST = ` - -GET _search -{ - "query": { - "match_all": {} - } -} - -`.trim(); - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const retry = getService('retry'); - const log = getService('log'); - const visualTesting = getService('visualTesting'); - const PageObjects = getPageObjects(['common', 'console']); - - describe.skip('console app', function describeIndexTests() { - before(async () => { - log.debug('navigateTo console'); - await PageObjects.common.navigateToApp('console'); - }); - - it('should show the default request', async () => { - // collapse the help pane because we only get the VISIBLE TEXT, not the part that is scrolled - await PageObjects.console.collapseHelp(); - await retry.try(async () => { - const actualRequest = await PageObjects.console.getRequest(); - log.debug(actualRequest); - expect(actualRequest.trim()).to.eql(DEFAULT_REQUEST); - }); - - await visualTesting.snapshot(); - }); - - it('default request response should include `"timed_out" : false`', async () => { - const expectedResponseContains = '"timed_out" : false,'; - await PageObjects.console.clickPlay(); - await retry.try(async () => { - const actualResponse = await PageObjects.console.getResponse(); - log.debug(actualResponse); - expect(actualResponse).to.contain(expectedResponseContains); - }); - }); - - it('settings should allow changing the text size', async () => { - await PageObjects.console.setFontSizeSetting(20); - await retry.try(async () => { - // the settings are not applied synchronously, so we retry for a time - expect(await PageObjects.console.getRequestFontSize()).to.be('20px'); - }); - - await PageObjects.console.setFontSizeSetting(24); - await retry.try(async () => { - // the settings are not applied synchronously, so we retry for a time - expect(await PageObjects.console.getRequestFontSize()).to.be('24px'); - }); - }); - }); -} diff --git a/test/visual_regression/tests/discover/chart_visualization.ts b/test/visual_regression/tests/discover/chart_visualization.ts deleted file mode 100644 index f8390064732b90..00000000000000 --- a/test/visual_regression/tests/discover/chart_visualization.ts +++ /dev/null @@ -1,117 +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 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 or the Server - * Side Public License, v 1. - */ - -import expect from '@kbn/expect'; - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const retry = getService('retry'); - const esArchiver = getService('esArchiver'); - const browser = getService('browser'); - const kibanaServer = getService('kibanaServer'); - const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); - const visualTesting = getService('visualTesting'); - const defaultSettings = { - defaultIndex: 'logstash-*', - 'discover:sampleSize': 1, - }; - - describe('discover', function describeIndexTests() { - before(async function () { - await kibanaServer.savedObjects.cleanStandardList(); - await kibanaServer.importExport.load( - 'test/functional/fixtures/kbn_archiver/discover/visual_regression' - ); - - // and load a set of makelogs data - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); - await kibanaServer.uiSettings.replace(defaultSettings); - await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - }); - - after(async function unloadMakelogs() { - await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); - await kibanaServer.savedObjects.cleanStandardList(); - }); - - async function refreshDiscover() { - await browser.refresh(); - await PageObjects.header.awaitKibanaChrome(); - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - await PageObjects.discover.waitUntilSearchingHasFinished(); - await PageObjects.discover.waitForChartLoadingComplete(1); - } - - async function takeSnapshot() { - await refreshDiscover(); - await visualTesting.snapshot({ - show: ['discoverChart'], - }); - } - - describe('query', function () { - this.tags(['skipFirefox']); - - it('should show bars in the correct time zone', async function () { - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - await PageObjects.discover.waitUntilSearchingHasFinished(); - await takeSnapshot(); - }); - - it('should show correct data for chart interval Hour', async function () { - await PageObjects.discover.setChartInterval('Hour'); - await takeSnapshot(); - }); - - it('should show correct data for chart interval Day', async function () { - await PageObjects.discover.setChartInterval('Day'); - await takeSnapshot(); - }); - - it('should show correct data for chart interval Week', async function () { - await PageObjects.discover.setChartInterval('Week'); - await takeSnapshot(); - }); - - it('browser back button should show previous interval Day', async function () { - await browser.goBack(); - await retry.try(async function tryingForTime() { - const actualInterval = await PageObjects.discover.getChartInterval(); - expect(actualInterval).to.be('Day'); - }); - await takeSnapshot(); - }); - - it('should show correct data for chart interval Month', async function () { - await PageObjects.discover.setChartInterval('Month'); - await takeSnapshot(); - }); - - it('should show correct data for chart interval Year', async function () { - await PageObjects.discover.setChartInterval('Year'); - await takeSnapshot(); - }); - - it('should show correct data for chart interval Auto', async function () { - await PageObjects.discover.setChartInterval('Auto'); - await takeSnapshot(); - }); - }); - - describe('time zone switch', () => { - it('should show bars in the correct time zone after switching', async function () { - await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'America/Phoenix' }); - await refreshDiscover(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await takeSnapshot(); - }); - }); - }); -} diff --git a/test/visual_regression/tests/discover/index.ts b/test/visual_regression/tests/discover/index.ts deleted file mode 100644 index 9142a430f963ba..00000000000000 --- a/test/visual_regression/tests/discover/index.ts +++ /dev/null @@ -1,25 +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 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 or the Server - * Side Public License, v 1. - */ - -import { DEFAULT_OPTIONS } from '../../services/visual_testing/visual_testing'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -// Width must be the same as visual_testing or canvas image widths will get skewed -const [SCREEN_WIDTH] = DEFAULT_OPTIONS.widths || []; - -export default function ({ getService, loadTestFile }: FtrProviderContext) { - const browser = getService('browser'); - - describe('discover app', function () { - before(function () { - return browser.setWindowSize(SCREEN_WIDTH, 1000); - }); - - loadTestFile(require.resolve('./chart_visualization')); - }); -} diff --git a/test/visual_regression/tests/vega/index.ts b/test/visual_regression/tests/vega/index.ts deleted file mode 100644 index 9ab4e199439a4d..00000000000000 --- a/test/visual_regression/tests/vega/index.ts +++ /dev/null @@ -1,25 +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 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 or the Server - * Side Public License, v 1. - */ - -import { DEFAULT_OPTIONS } from '../../services/visual_testing/visual_testing'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -// Width must be the same as visual_testing or canvas image widths will get skewed -const [SCREEN_WIDTH] = DEFAULT_OPTIONS.widths || []; - -export default function ({ getService, loadTestFile }: FtrProviderContext) { - const browser = getService('browser'); - - describe('vega app', function () { - before(function () { - return browser.setWindowSize(SCREEN_WIDTH, 1000); - }); - - loadTestFile(require.resolve('./vega_map_visualization')); - }); -} diff --git a/test/visual_regression/tests/vega/vega_map_visualization.ts b/test/visual_regression/tests/vega/vega_map_visualization.ts deleted file mode 100644 index d891e7f2bab6b0..00000000000000 --- a/test/visual_regression/tests/vega/vega_map_visualization.ts +++ /dev/null @@ -1,39 +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 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 or the Server - * Side Public License, v 1. - */ - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); - const PageObjects = getPageObjects(['common', 'visualize', 'visChart', 'visEditor', 'vegaChart']); - const visualTesting = getService('visualTesting'); - - describe('vega chart in visualize app', () => { - before(async () => { - await esArchiver.loadIfNeeded( - 'test/functional/fixtures/es_archiver/kibana_sample_data_flights' - ); - await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json'); - }); - - after(async () => { - await esArchiver.unload('test/functional/fixtures/es_archiver/kibana_sample_data_flights'); - await kibanaServer.importExport.unload( - 'test/functional/fixtures/kbn_archiver/visualize.json' - ); - }); - - it('should show map with vega layer', async function () { - await PageObjects.visualize.gotoVisualizationLandingPage(); - await PageObjects.visualize.openSavedVisualization('VegaMap'); - await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - await visualTesting.snapshot(); - }); - }); -} diff --git a/x-pack/packages/ml/agg_utils/BUILD.bazel b/x-pack/packages/ml/agg_utils/BUILD.bazel index b9b4533b56e37d..8841369749200d 100644 --- a/x-pack/packages/ml/agg_utils/BUILD.bazel +++ b/x-pack/packages/ml/agg_utils/BUILD.bazel @@ -68,6 +68,7 @@ TYPES_DEPS = [ "@npm//@types/lodash", "@npm//@elastic/elasticsearch", "@npm//tslib", + "//packages/core/elasticsearch/core-elasticsearch-server:npm_module_types", "//packages/kbn-field-types:npm_module_types", "//x-pack/packages/ml/is_populated_object:npm_module_types", "//x-pack/packages/ml/string_hash:npm_module_types", diff --git a/x-pack/packages/ml/agg_utils/src/fetch_agg_intervals.ts b/x-pack/packages/ml/agg_utils/src/fetch_agg_intervals.ts index 1d6b3f781f901f..338f55ad754c29 100644 --- a/x-pack/packages/ml/agg_utils/src/fetch_agg_intervals.ts +++ b/x-pack/packages/ml/agg_utils/src/fetch_agg_intervals.ts @@ -9,13 +9,14 @@ import { get } from 'lodash'; import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { KBN_FIELD_TYPES } from '@kbn/field-types'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { stringHash } from '@kbn/ml-string-hash'; import { buildSamplerAggregation } from './build_sampler_aggregation'; import { getSamplerAggregationsResponsePath } from './get_sampler_aggregations_response_path'; -import type { ElasticsearchClient, HistogramField, NumericColumnStatsMap } from './types'; +import type { HistogramField, NumericColumnStatsMap } from './types'; const MAX_CHART_COLUMNS = 20; diff --git a/x-pack/packages/ml/agg_utils/src/fetch_histograms_for_fields.ts b/x-pack/packages/ml/agg_utils/src/fetch_histograms_for_fields.ts index 1e8e2667c0aac0..a921eaeae370bb 100644 --- a/x-pack/packages/ml/agg_utils/src/fetch_histograms_for_fields.ts +++ b/x-pack/packages/ml/agg_utils/src/fetch_histograms_for_fields.ts @@ -9,6 +9,7 @@ import get from 'lodash/get'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { KBN_FIELD_TYPES } from '@kbn/field-types'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { stringHash } from '@kbn/ml-string-hash'; @@ -18,7 +19,6 @@ import { fetchAggIntervals } from './fetch_agg_intervals'; import { getSamplerAggregationsResponsePath } from './get_sampler_aggregations_response_path'; import type { AggCardinality, - ElasticsearchClient, HistogramField, NumericColumnStats, NumericColumnStatsMap, diff --git a/x-pack/packages/ml/agg_utils/src/types.ts b/x-pack/packages/ml/agg_utils/src/types.ts index 352616d74cb3e6..173c3b69ba7614 100644 --- a/x-pack/packages/ml/agg_utils/src/types.ts +++ b/x-pack/packages/ml/agg_utils/src/types.ts @@ -5,23 +5,8 @@ * 2.0. */ -import type { Client } from '@elastic/elasticsearch'; import { KBN_FIELD_TYPES } from '@kbn/field-types'; -// TODO Temporary type definition until we can import from `@kbn/core`. -// Copied from src/core/server/elasticsearch/client/types.ts -// as these types aren't part of any package yet. Once they are, remove this completely - -/** - * Client used to query the elasticsearch cluster. - * @deprecated At some point use the one from src/core/server/elasticsearch/client/types.ts when it is made into a package. If it never is, then keep using this one. - * @public - */ -export type ElasticsearchClient = Omit< - Client, - 'connectionPool' | 'serializer' | 'extend' | 'close' | 'diagnostic' ->; - interface FieldAggCardinality { field: string; percent?: any; diff --git a/x-pack/packages/ml/aiops_utils/BUILD.bazel b/x-pack/packages/ml/aiops_utils/BUILD.bazel index a7f8d5849ccb34..0e8edc688c6179 100644 --- a/x-pack/packages/ml/aiops_utils/BUILD.bazel +++ b/x-pack/packages/ml/aiops_utils/BUILD.bazel @@ -64,6 +64,7 @@ TYPES_DEPS = [ "@npm//@types/node", "@npm//@types/jest", "@npm//@types/react", + "//packages/core/elasticsearch/core-elasticsearch-server:npm_module_types", "//packages/kbn-logging:npm_module_types" ] diff --git a/x-pack/packages/ml/aiops_utils/src/accept_compression.ts b/x-pack/packages/ml/aiops_utils/src/accept_compression.ts index 690f5d21c52a8b..40424fec7ed11a 100644 --- a/x-pack/packages/ml/aiops_utils/src/accept_compression.ts +++ b/x-pack/packages/ml/aiops_utils/src/accept_compression.ts @@ -5,12 +5,7 @@ * 2.0. */ -/** - * TODO: Replace these with kbn packaged versions once we have those available to us. - * At the moment imports from runtime plugins into packages are not supported. - * import type { Headers } from '@kbn/core/server'; - */ -type Headers = Record; +import type { Headers } from '@kbn/core-http-server'; function containsGzip(s: string) { return s diff --git a/x-pack/packages/ml/aiops_utils/src/stream_factory.test.ts b/x-pack/packages/ml/aiops_utils/src/stream_factory.test.ts index a0c5212244ad6e..1e6d7b40b22d05 100644 --- a/x-pack/packages/ml/aiops_utils/src/stream_factory.test.ts +++ b/x-pack/packages/ml/aiops_utils/src/stream_factory.test.ts @@ -44,7 +44,12 @@ describe('streamFactory', () => { streamResult += chunk.toString('utf8'); } - expect(responseWithHeaders.headers).toBe(undefined); + expect(responseWithHeaders.headers).toStrictEqual({ + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + 'Transfer-Encoding': 'chunked', + 'X-Accel-Buffering': 'no', + }); expect(streamResult).toBe('push1push2'); }); @@ -65,7 +70,12 @@ describe('streamFactory', () => { const parsedItems = streamItems.map((d) => JSON.parse(d)); - expect(responseWithHeaders.headers).toBe(undefined); + expect(responseWithHeaders.headers).toStrictEqual({ + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + 'Transfer-Encoding': 'chunked', + 'X-Accel-Buffering': 'no', + }); expect(parsedItems).toHaveLength(2); expect(parsedItems[0]).toStrictEqual(mockItem1); expect(parsedItems[1]).toStrictEqual(mockItem2); @@ -105,7 +115,13 @@ describe('streamFactory', () => { const streamResult = decoded.toString('utf8'); - expect(responseWithHeaders.headers).toStrictEqual({ 'content-encoding': 'gzip' }); + expect(responseWithHeaders.headers).toStrictEqual({ + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + 'content-encoding': 'gzip', + 'Transfer-Encoding': 'chunked', + 'X-Accel-Buffering': 'no', + }); expect(streamResult).toBe('push1push2'); done(); @@ -143,7 +159,13 @@ describe('streamFactory', () => { const parsedItems = streamItems.map((d) => JSON.parse(d)); - expect(responseWithHeaders.headers).toStrictEqual({ 'content-encoding': 'gzip' }); + expect(responseWithHeaders.headers).toStrictEqual({ + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + 'content-encoding': 'gzip', + 'Transfer-Encoding': 'chunked', + 'X-Accel-Buffering': 'no', + }); expect(parsedItems).toHaveLength(2); expect(parsedItems[0]).toStrictEqual(mockItem1); expect(parsedItems[1]).toStrictEqual(mockItem2); diff --git a/x-pack/packages/ml/aiops_utils/src/stream_factory.ts b/x-pack/packages/ml/aiops_utils/src/stream_factory.ts index 9df9702eb08707..16bc2abe193063 100644 --- a/x-pack/packages/ml/aiops_utils/src/stream_factory.ts +++ b/x-pack/packages/ml/aiops_utils/src/stream_factory.ts @@ -9,16 +9,10 @@ import { Stream } from 'stream'; import * as zlib from 'zlib'; import type { Logger } from '@kbn/logging'; +import type { Headers, ResponseHeaders } from '@kbn/core-http-server'; import { acceptCompression } from './accept_compression'; -/** - * TODO: Replace these with kbn packaged versions once we have those available to us. - * At the moment imports from runtime plugins into packages are not supported. - * import type { Headers } from '@kbn/core/server'; - */ -type Headers = Record; - // We need this otherwise Kibana server will crash with a 'ERR_METHOD_NOT_IMPLEMENTED' error. class ResponseStream extends Stream.PassThrough { flush() {} @@ -35,9 +29,7 @@ interface StreamFactoryReturnType { push: (d: T) => void; responseWithHeaders: { body: zlib.Gzip | ResponseStream; - // TODO: Replace these with kbn packaged versions once we have those available to us. - // At the moment imports from runtime plugins into packages are not supported. - headers?: any; + headers?: ResponseHeaders; }; } @@ -106,13 +98,16 @@ export function streamFactory( const responseWithHeaders: StreamFactoryReturnType['responseWithHeaders'] = { body: stream, - ...(isCompressed - ? { - headers: { - 'content-encoding': 'gzip', - }, - } - : {}), + headers: { + ...(isCompressed ? { 'content-encoding': 'gzip' } : {}), + + // This disables response buffering on proxy servers (Nginx, uwsgi, fastcgi, etc.) + // Otherwise, those proxies buffer responses up to 4/8 KiB. + 'X-Accel-Buffering': 'no', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + 'Transfer-Encoding': 'chunked', + }, }; return { DELIMITER, end, push, responseWithHeaders }; diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 7d9dc9d4060350..213ec6d8f23ce5 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -260,7 +260,9 @@ In addition to the documented configurations, several built in action type offer ## ServiceNow ITSM -The [ServiceNow ITSM user documentation `params`](https://www.elastic.co/guide/en/kibana/master/servicenow-action-type.html) lists configuration properties for the `pushToService` subaction. In addition, several other subaction types are available. +Refer to the [Run connector API documentation](https://www.elastic.co/guide/en/kibana/master/execute-connector-api.html#execute-connector-api-request-body) +for the full list of properties. + ### `params` | Property | Description | Type | @@ -311,7 +313,8 @@ No parameters for the `getFields` subaction. Provide an empty object `{}`. ## ServiceNow Sec Ops -The [ServiceNow SecOps user documentation `params`](https://www.elastic.co/guide/en/kibana/master/servicenow-sir-action-type.html) lists configuration properties for the `pushToService` subaction. In addition, several other subaction types are available. +Refer to the [Run connector API documentation](https://www.elastic.co/guide/en/kibana/master/execute-connector-api.html#execute-connector-api-request-body) +for the full list of properties. ### `params` @@ -364,7 +367,9 @@ No parameters for the `getFields` subaction. Provide an empty object `{}`. --- ## ServiceNow ITOM -The [ServiceNow ITOM user documentation `params`](https://www.elastic.co/guide/en/kibana/master/servicenow-itom-action-type.html) lists configuration properties for the `addEvent` subaction. In addition, several other subaction types are available. +Refer to the [Run connector API documentation](https://www.elastic.co/guide/en/kibana/master/execute-connector-api.html#execute-connector-api-request-body) +for the full list of properties. + ### `params` | Property | Description | Type | diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index d2777a8e3ae68c..73d5b32d077a98 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -78,7 +78,7 @@ import { // We'll set this max setting assuming it's never reached. export const MAX_ACTIONS_RETURNED = 10000; -interface ActionUpdate extends SavedObjectAttributes { +interface ActionUpdate { name: string; config: SavedObjectAttributes; secrets: SavedObjectAttributes; diff --git a/x-pack/plugins/alerting/common/execution_log_types.ts b/x-pack/plugins/alerting/common/execution_log_types.ts index 41406bb6c9dc1e..1938d8be4acd35 100644 --- a/x-pack/plugins/alerting/common/execution_log_types.ts +++ b/x-pack/plugins/alerting/common/execution_log_types.ts @@ -49,6 +49,7 @@ export interface IExecutionLog { schedule_delay_ms: number; timed_out: boolean; rule_id: string; + rule_name: string; } export interface IExecutionErrors { diff --git a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts index bd3fc05c6d8f75..e48483785490a6 100644 --- a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts +++ b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts @@ -278,6 +278,7 @@ describe('getExecutionLogAggregation', () => { 'error.message', 'kibana.version', 'rule.id', + 'rule.name', ], }, }, @@ -482,6 +483,7 @@ describe('getExecutionLogAggregation', () => { 'error.message', 'kibana.version', 'rule.id', + 'rule.name', ], }, }, @@ -686,6 +688,7 @@ describe('getExecutionLogAggregation', () => { 'error.message', 'kibana.version', 'rule.id', + 'rule.name', ], }, }, @@ -776,7 +779,7 @@ describe('formatExecutionLogResult', () => { _id: 'S4wIZX8B8TGQpG7XQZns', _score: 1.0, _source: { - rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef' }, + rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', name: 'rule_name' }, event: { outcome: 'success', }, @@ -860,7 +863,7 @@ describe('formatExecutionLogResult', () => { _id: 'a4wIZX8B8TGQpG7Xwpnz', _score: 1.0, _source: { - rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef' }, + rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', name: 'rule_name' }, event: { outcome: 'success', @@ -940,6 +943,7 @@ describe('formatExecutionLogResult', () => { timed_out: false, schedule_delay_ms: 3074, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', + rule_name: 'rule_name', }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -961,6 +965,7 @@ describe('formatExecutionLogResult', () => { timed_out: false, schedule_delay_ms: 3126, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', + rule_name: 'rule_name', }, ], }); @@ -1015,7 +1020,7 @@ describe('formatExecutionLogResult', () => { _id: 'S4wIZX8B8TGQpG7XQZns', _score: 1.0, _source: { - rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef' }, + rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', name: 'rule_name' }, event: { outcome: 'failure', }, @@ -1102,7 +1107,7 @@ describe('formatExecutionLogResult', () => { _id: 'a4wIZX8B8TGQpG7Xwpnz', _score: 1.0, _source: { - rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef' }, + rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', name: 'rule_name' }, event: { outcome: 'success', }, @@ -1181,6 +1186,7 @@ describe('formatExecutionLogResult', () => { timed_out: false, schedule_delay_ms: 3074, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', + rule_name: 'rule_name', }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -1202,6 +1208,7 @@ describe('formatExecutionLogResult', () => { timed_out: false, schedule_delay_ms: 3126, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', + rule_name: 'rule_name', }, ], }); @@ -1256,7 +1263,7 @@ describe('formatExecutionLogResult', () => { _id: 'dJkWa38B1ylB1EvsAckB', _score: 1.0, _source: { - rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef' }, + rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', name: 'rule_name' }, event: { outcome: 'success', }, @@ -1335,7 +1342,7 @@ describe('formatExecutionLogResult', () => { _id: 'a4wIZX8B8TGQpG7Xwpnz', _score: 1.0, _source: { - rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef' }, + rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', name: 'rule_name' }, event: { outcome: 'success', }, @@ -1414,6 +1421,7 @@ describe('formatExecutionLogResult', () => { timed_out: true, schedule_delay_ms: 3074, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', + rule_name: 'rule_name', }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -1435,6 +1443,7 @@ describe('formatExecutionLogResult', () => { timed_out: false, schedule_delay_ms: 3126, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', + rule_name: 'rule_name', }, ], }); @@ -1489,7 +1498,7 @@ describe('formatExecutionLogResult', () => { _id: '7xKcb38BcntAq5ycFwiu', _score: 1.0, _source: { - rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef' }, + rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', name: 'rule_name' }, event: { outcome: 'success', }, @@ -1573,7 +1582,7 @@ describe('formatExecutionLogResult', () => { _id: 'zRKbb38BcntAq5ycOwgk', _score: 1.0, _source: { - rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef' }, + rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', name: 'rule_name' }, event: { outcome: 'success', }, @@ -1652,6 +1661,7 @@ describe('formatExecutionLogResult', () => { timed_out: false, schedule_delay_ms: 3126, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', + rule_name: 'rule_name', }, { id: '61bb867b-661a-471f-bf92-23471afa10b3', @@ -1673,6 +1683,7 @@ describe('formatExecutionLogResult', () => { timed_out: false, schedule_delay_ms: 3133, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', + rule_name: 'rule_name', }, ], }); diff --git a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts index 14d67807a86af4..0854488d5f29ee 100644 --- a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts +++ b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts @@ -17,6 +17,7 @@ import { IExecutionLog, IExecutionLogResult } from '../../common'; const DEFAULT_MAX_BUCKETS_LIMIT = 1000; // do not retrieve more than this number of executions const RULE_ID_FIELD = 'rule.id'; +const RULE_NAME_FIELD = 'rule.name'; const PROVIDER_FIELD = 'event.provider'; const START_FIELD = 'event.start'; const ACTION_FIELD = 'event.action'; @@ -265,6 +266,7 @@ export function getExecutionLogAggregation({ ERROR_MESSAGE_FIELD, VERSION_FIELD, RULE_ID_FIELD, + RULE_NAME_FIELD, ], }, }, @@ -336,6 +338,7 @@ function formatExecutionLogAggBucket(bucket: IExecutionUuidAggBucket): IExecutio const version = outcomeAndMessage ? outcomeAndMessage?.kibana?.version ?? '' : ''; const ruleId = outcomeAndMessage ? outcomeAndMessage?.rule?.id ?? '' : ''; + const ruleName = outcomeAndMessage ? outcomeAndMessage?.rule?.name ?? '' : ''; return { id: bucket?.key ?? '', timestamp: bucket?.ruleExecution?.executeStartTime.value_as_string ?? '', @@ -355,6 +358,7 @@ function formatExecutionLogAggBucket(bucket: IExecutionUuidAggBucket): IExecutio schedule_delay_ms: scheduleDelayUs / Millis2Nanos, timed_out: timedOut, rule_id: ruleId, + rule_name: ruleName, }; } diff --git a/x-pack/plugins/alerting/server/routes/get_global_execution_logs.test.ts b/x-pack/plugins/alerting/server/routes/get_global_execution_logs.test.ts index 4c7f6c0a2750e0..43b08ed0787e29 100644 --- a/x-pack/plugins/alerting/server/routes/get_global_execution_logs.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_global_execution_logs.test.ts @@ -46,6 +46,7 @@ describe('getRuleExecutionLogRoute', () => { timed_out: false, schedule_delay_ms: 3126, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', + rule_name: 'rule-name', }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -67,6 +68,7 @@ describe('getRuleExecutionLogRoute', () => { timed_out: false, schedule_delay_ms: 3008, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', + rule_name: 'rule-name', }, ], }; diff --git a/x-pack/plugins/alerting/server/routes/get_rule_execution_log.test.ts b/x-pack/plugins/alerting/server/routes/get_rule_execution_log.test.ts index 9c1be8628c8234..048da6cbabeb35 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule_execution_log.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule_execution_log.test.ts @@ -47,6 +47,7 @@ describe('getRuleExecutionLogRoute', () => { timed_out: false, schedule_delay_ms: 3126, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', + rule_name: 'rule_name', }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -68,6 +69,7 @@ describe('getRuleExecutionLogRoute', () => { timed_out: false, schedule_delay_ms: 3008, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', + rule_name: 'rule_name', }, ], }; diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index f0cf88615047dc..55f9ce0da063c1 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -384,7 +384,7 @@ export interface GetActionErrorLogByIdParams { sort: estypes.Sort; } -interface ScheduleRuleOptions { +interface ScheduleTaskOptions { id: string; consumer: string; ruleTypeId: string; @@ -589,7 +589,7 @@ export class RulesClient { if (data.enabled) { let scheduledTask; try { - scheduledTask = await this.scheduleRule({ + scheduledTask = await this.scheduleTask({ id: createdAlert.id, consumer: data.consumer, ruleTypeId: rawRule.alertTypeId, @@ -944,32 +944,7 @@ export class RulesClient { } ); - const formattedResult = formatExecutionLogResult(aggResult); - const ruleIds = [...new Set(formattedResult.data.map((l) => l.rule_id))].filter( - Boolean - ) as string[]; - const ruleNameIdEntries = await Promise.all( - ruleIds.map(async (id) => { - try { - const result = await this.get({ id }); - return [id, result.name]; - } catch (e) { - return [id, id]; - } - }) - ); - const ruleNameIdMap: Record = ruleNameIdEntries.reduce( - (result, [key, val]) => ({ ...result, [key]: val }), - {} - ); - - return { - ...formattedResult, - data: formattedResult.data.map((entry) => ({ - ...entry, - rule_name: ruleNameIdMap[entry.rule_id!], - })), - }; + return formatExecutionLogResult(aggResult); } catch (err) { this.logger.debug( `rulesClient.getGlobalExecutionLogWithAuth(): error searching global event log: ${err.message}` @@ -2138,7 +2113,24 @@ export class RulesClient { } catch (e) { throw e; } - const scheduledTask = await this.scheduleRule({ + } + + let scheduledTaskIdToCreate: string | null = null; + if (attributes.scheduledTaskId) { + // If scheduledTaskId defined in rule SO, make sure it exists + try { + await this.taskManager.get(attributes.scheduledTaskId); + } catch (err) { + scheduledTaskIdToCreate = id; + } + } else { + // If scheduledTaskId doesn't exist in rule SO, set it to rule ID + scheduledTaskIdToCreate = id; + } + + if (scheduledTaskIdToCreate) { + // Schedule the task if it doesn't exist + const scheduledTask = await this.scheduleTask({ id, consumer: attributes.consumer, ruleTypeId: attributes.alertTypeId, @@ -2148,6 +2140,9 @@ export class RulesClient { await this.unsecuredSavedObjectsClient.update('alert', id, { scheduledTaskId: scheduledTask.id, }); + } else { + // Task exists so set enabled to true + await this.taskManager.bulkEnableDisable([attributes.scheduledTaskId!], true); } } @@ -2282,14 +2277,21 @@ export class RulesClient { this.updateMeta({ ...attributes, enabled: false, - scheduledTaskId: null, + scheduledTaskId: attributes.scheduledTaskId === id ? attributes.scheduledTaskId : null, updatedBy: await this.getUserName(), updatedAt: new Date().toISOString(), }), { version } ); + + // If the scheduledTaskId does not match the rule id, we should + // remove the task, otherwise mark the task as disabled if (attributes.scheduledTaskId) { - await this.taskManager.removeIfExists(attributes.scheduledTaskId); + if (attributes.scheduledTaskId !== id) { + await this.taskManager.removeIfExists(attributes.scheduledTaskId); + } else { + await this.taskManager.bulkEnableDisable([attributes.scheduledTaskId], false); + } } } } @@ -2767,7 +2769,7 @@ export class RulesClient { return this.spaceId; } - private async scheduleRule(opts: ScheduleRuleOptions) { + private async scheduleTask(opts: ScheduleTaskOptions) { const { id, consumer, ruleTypeId, schedule, throwOnConflict } = opts; const taskInstance = { id, // use the same ID for task document as the rule @@ -2784,6 +2786,7 @@ export class RulesClient { alertInstances: {}, }, scope: ['alerting'], + enabled: true, }; try { return await this.taskManager.schedule(taskInstance); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts index b29d41f183b735..f5192bf6cbe659 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts @@ -463,6 +463,7 @@ describe('create()', () => { expect(taskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { + "enabled": true, "id": "1", "params": Object { "alertId": "1", diff --git a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts index a193733aff26fa..499f1c2e8454dc 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts @@ -60,7 +60,7 @@ const rulesClientParams: jest.Mocked = { beforeEach(() => { getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry); taskManager.get.mockResolvedValue({ - id: 'task-123', + id: '1', taskType: 'alerting:123', scheduledAt: new Date(), attempts: 1, @@ -81,7 +81,7 @@ setGlobalDate(); describe('disable()', () => { let rulesClient: RulesClient; - const existingAlert = { + const existingRule = { id: '1', type: 'alert', attributes: { @@ -89,7 +89,7 @@ describe('disable()', () => { schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: true, - scheduledTaskId: 'task-123', + scheduledTaskId: '1', actions: [ { group: 'default', @@ -105,10 +105,10 @@ describe('disable()', () => { version: '123', references: [], }; - const existingDecryptedAlert = { - ...existingAlert, + const existingDecryptedRule = { + ...existingRule, attributes: { - ...existingAlert.attributes, + ...existingRule.attributes, apiKey: Buffer.from('123:abc').toString('base64'), apiKeyOwner: 'elastic', }, @@ -118,12 +118,12 @@ describe('disable()', () => { beforeEach(() => { rulesClient = new RulesClient(rulesClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingRule); + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedRule); }); describe('authorization', () => { - test('ensures user is authorised to disable this type of alert under the consumer', async () => { + test('ensures user is authorised to disable this type of rule under the consumer', async () => { await rulesClient.disable({ id: '1' }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ @@ -134,7 +134,7 @@ describe('disable()', () => { }); }); - test('throws when user is not authorised to disable this type of alert', async () => { + test('throws when user is not authorised to disable this type of rule', async () => { authorization.ensureAuthorized.mockRejectedValue( new Error(`Unauthorized to disable a "myType" alert for "myApp"`) ); @@ -191,7 +191,7 @@ describe('disable()', () => { }); }); - test('disables an alert', async () => { + test('disables an rule', async () => { await rulesClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { @@ -208,7 +208,7 @@ describe('disable()', () => { meta: { versionApiKeyLastmodified: 'v7.10.0', }, - scheduledTaskId: null, + scheduledTaskId: '1', apiKey: 'MTIzOmFiYw==', apiKeyOwner: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', @@ -229,11 +229,12 @@ describe('disable()', () => { version: '123', } ); - expect(taskManager.removeIfExists).toHaveBeenCalledWith('task-123'); + expect(taskManager.bulkEnableDisable).toHaveBeenCalledWith(['1'], false); + expect(taskManager.removeIfExists).not.toHaveBeenCalledWith(); }); test('disables the rule with calling event log to "recover" the alert instances from the task state', async () => { - const scheduledTaskId = 'task-123'; + const scheduledTaskId = '1'; taskManager.get.mockResolvedValue({ id: scheduledTaskId, taskType: 'alerting:123', @@ -278,7 +279,7 @@ describe('disable()', () => { meta: { versionApiKeyLastmodified: 'v7.10.0', }, - scheduledTaskId: null, + scheduledTaskId: '1', apiKey: 'MTIzOmFiYw==', apiKeyOwner: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', @@ -299,7 +300,8 @@ describe('disable()', () => { version: '123', } ); - expect(taskManager.removeIfExists).toHaveBeenCalledWith('task-123'); + expect(taskManager.bulkEnableDisable).toHaveBeenCalledWith(['1'], false); + expect(taskManager.removeIfExists).not.toHaveBeenCalledWith(); expect(eventLogger.logEvent).toHaveBeenCalledTimes(1); expect(eventLogger.logEvent.mock.calls[0][0]).toStrictEqual({ @@ -359,7 +361,7 @@ describe('disable()', () => { meta: { versionApiKeyLastmodified: 'v7.10.0', }, - scheduledTaskId: null, + scheduledTaskId: '1', apiKey: 'MTIzOmFiYw==', apiKeyOwner: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', @@ -380,7 +382,8 @@ describe('disable()', () => { version: '123', } ); - expect(taskManager.removeIfExists).toHaveBeenCalledWith('task-123'); + expect(taskManager.bulkEnableDisable).toHaveBeenCalledWith(['1'], false); + expect(taskManager.removeIfExists).not.toHaveBeenCalledWith(); expect(eventLogger.logEvent).toHaveBeenCalledTimes(0); expect(rulesClientParams.logger.warn).toHaveBeenCalledWith( @@ -403,7 +406,7 @@ describe('disable()', () => { schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: false, - scheduledTaskId: null, + scheduledTaskId: '1', updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', actions: [ @@ -422,14 +425,15 @@ describe('disable()', () => { version: '123', } ); - expect(taskManager.removeIfExists).toHaveBeenCalledWith('task-123'); + expect(taskManager.bulkEnableDisable).toHaveBeenCalledWith(['1'], false); + expect(taskManager.removeIfExists).not.toHaveBeenCalledWith(); }); - test(`doesn't disable already disabled alerts`, async () => { + test(`doesn't disable already disabled rules`, async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ - ...existingDecryptedAlert, + ...existingDecryptedRule, attributes: { - ...existingDecryptedAlert.attributes, + ...existingDecryptedRule.attributes, actions: [], enabled: false, }, @@ -437,7 +441,8 @@ describe('disable()', () => { await rulesClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); - expect(taskManager.removeIfExists).not.toHaveBeenCalled(); + expect(taskManager.bulkEnableDisable).not.toHaveBeenCalled(); + expect(taskManager.removeIfExists).not.toHaveBeenCalledWith(); }); test('swallows error when failing to load decrypted saved object', async () => { @@ -445,7 +450,8 @@ describe('disable()', () => { await rulesClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); - expect(taskManager.removeIfExists).toHaveBeenCalled(); + expect(taskManager.bulkEnableDisable).toHaveBeenCalled(); + expect(taskManager.removeIfExists).not.toHaveBeenCalledWith(); expect(rulesClientParams.logger.error).toHaveBeenCalledWith( 'disable(): Failed to load API key of alert 1: Fail' ); @@ -457,13 +463,106 @@ describe('disable()', () => { await expect(rulesClient.disable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"Failed to update"` ); + expect(taskManager.bulkEnableDisable).not.toHaveBeenCalled(); + expect(taskManager.removeIfExists).not.toHaveBeenCalledWith(); }); - test('throws when failing to remove task from task manager', async () => { - taskManager.removeIfExists.mockRejectedValueOnce(new Error('Failed to remove task')); + test('throws when failing to disable task', async () => { + taskManager.bulkEnableDisable.mockRejectedValueOnce(new Error('Failed to disable task')); + await expect(rulesClient.disable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to disable task"` + ); + expect(taskManager.removeIfExists).not.toHaveBeenCalledWith(); + }); + + test('removes task document if scheduled task id does not match rule id', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ + ...existingRule, + attributes: { + ...existingRule.attributes, + scheduledTaskId: 'task-123', + }, + }); + await rulesClient.disable({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + consumer: 'myApp', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + enabled: false, + scheduledTaskId: null, + updatedAt: '2019-02-12T21:01:22.479Z', + updatedBy: 'elastic', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + }, + { + version: '123', + } + ); + expect(taskManager.bulkEnableDisable).not.toHaveBeenCalled(); + expect(taskManager.removeIfExists).toHaveBeenCalledWith('task-123'); + }); + + test('throws when failing to remove existing task', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ + ...existingRule, + attributes: { + ...existingRule.attributes, + scheduledTaskId: 'task-123', + }, + }); + taskManager.removeIfExists.mockRejectedValueOnce(new Error('Failed to remove task')); await expect(rulesClient.disable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"Failed to remove task"` ); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + consumer: 'myApp', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + enabled: false, + scheduledTaskId: null, + updatedAt: '2019-02-12T21:01:22.479Z', + updatedBy: 'elastic', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + }, + { + version: '123', + } + ); + expect(taskManager.bulkEnableDisable).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts index 8923031ab6b87d..b8d259bd6a6829 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts @@ -63,6 +63,7 @@ describe('enable()', () => { consumer: 'myApp', schedule: { interval: '10s' }, alertTypeId: 'myType', + scheduledTaskId: 'task-123', enabled: false, apiKey: 'MTIzOmFiYw==', apiKeyOwner: 'elastic', @@ -91,7 +92,25 @@ describe('enable()', () => { }, }; + const mockTask = { + id: 'task-123', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: { + alertId: '1', + }, + ownerId: null, + enabled: false, + }; + beforeEach(() => { + jest.resetAllMocks(); getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry); (auditLogger.log as jest.Mock).mockClear(); rulesClient = new RulesClient(rulesClientParams); @@ -100,19 +119,7 @@ describe('enable()', () => { rulesClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false, }); - taskManager.schedule.mockResolvedValue({ - id: '1', - scheduledAt: new Date(), - attempts: 0, - status: TaskStatus.Idle, - runAt: new Date(), - state: {}, - params: {}, - taskType: '', - startedAt: null, - retryAt: null, - ownerId: null, - }); + taskManager.get.mockResolvedValue(mockTask); }); describe('authorization', () => { @@ -208,6 +215,7 @@ describe('enable()', () => { updatedBy: 'elastic', apiKey: 'MTIzOmFiYw==', apiKeyOwner: 'elastic', + scheduledTaskId: 'task-123', actions: [ { group: 'default', @@ -231,27 +239,7 @@ describe('enable()', () => { version: '123', } ); - expect(taskManager.schedule).toHaveBeenCalledWith({ - id: '1', - taskType: `alerting:myType`, - params: { - alertId: '1', - spaceId: 'default', - consumer: 'myApp', - }, - schedule: { - interval: '10s', - }, - state: { - alertInstances: {}, - alertTypeState: {}, - previousStartedAt: null, - }, - scope: ['alerting'], - }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { - scheduledTaskId: '1', - }); + expect(taskManager.bulkEnableDisable).toHaveBeenCalledWith(['task-123'], true); }); test('enables a rule that does not have an apiKey', async () => { @@ -283,6 +271,7 @@ describe('enable()', () => { updatedBy: 'elastic', apiKey: 'MTIzOmFiYw==', apiKeyOwner: 'elastic', + scheduledTaskId: 'task-123', actions: [ { group: 'default', @@ -306,9 +295,10 @@ describe('enable()', () => { version: '123', } ); + expect(taskManager.bulkEnableDisable).toHaveBeenCalledWith(['task-123'], true); }); - test(`doesn't enable already enabled alerts`, async () => { + test(`doesn't update already enabled alerts but ensures task is enabled`, async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ ...existingRuleWithoutApiKey, attributes: { @@ -321,7 +311,7 @@ describe('enable()', () => { expect(rulesClientParams.getUserName).not.toHaveBeenCalled(); expect(rulesClientParams.createAPIKey).not.toHaveBeenCalled(); expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); - expect(taskManager.schedule).not.toHaveBeenCalled(); + expect(taskManager.bulkEnableDisable).toHaveBeenCalledWith(['task-123'], true); }); test('sets API key when createAPIKey returns one', async () => { @@ -345,6 +335,7 @@ describe('enable()', () => { }, apiKey: Buffer.from('123:abc').toString('base64'), apiKeyOwner: 'elastic', + scheduledTaskId: 'task-123', updatedBy: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', actions: [ @@ -370,6 +361,7 @@ describe('enable()', () => { version: '123', } ); + expect(taskManager.bulkEnableDisable).toHaveBeenCalledWith(['task-123'], true); }); test('throws an error if API key creation throws', async () => { @@ -381,6 +373,7 @@ describe('enable()', () => { await expect( async () => await rulesClient.enable({ id: '1' }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"Error creating API key for rule: no"`); + expect(taskManager.bulkEnableDisable).not.toHaveBeenCalled(); }); test('falls back when failing to getDecryptedAsInternalUser', async () => { @@ -391,6 +384,7 @@ describe('enable()', () => { expect(rulesClientParams.logger.error).toHaveBeenCalledWith( 'enable(): Failed to load API key of alert 1: Fail' ); + expect(taskManager.bulkEnableDisable).toHaveBeenCalledWith(['task-123'], true); }); test('throws error when failing to load the saved object using SOC', async () => { @@ -403,10 +397,10 @@ describe('enable()', () => { expect(rulesClientParams.getUserName).not.toHaveBeenCalled(); expect(rulesClientParams.createAPIKey).not.toHaveBeenCalled(); expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); - expect(taskManager.schedule).not.toHaveBeenCalled(); + expect(taskManager.bulkEnableDisable).not.toHaveBeenCalled(); }); - test('throws error when failing to update the first time', async () => { + test('throws when unsecuredSavedObjectsClient update fails', async () => { rulesClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, result: { id: '123', name: '123', api_key: 'abc' }, @@ -419,100 +413,102 @@ describe('enable()', () => { ); expect(rulesClientParams.getUserName).toHaveBeenCalled(); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(taskManager.schedule).not.toHaveBeenCalled(); + expect(taskManager.bulkEnableDisable).not.toHaveBeenCalled(); }); - test('throws error when failing to update the second time', async () => { - unsecuredSavedObjectsClient.update.mockReset(); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ - ...existingRuleWithoutApiKey, - attributes: { - ...existingRuleWithoutApiKey.attributes, - enabled: true, - }, + test('enables task when scheduledTaskId is defined and task exists', async () => { + await rulesClient.enable({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', }); - unsecuredSavedObjectsClient.update.mockRejectedValueOnce( - new Error('Fail to update second time') - ); - - await expect(rulesClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Fail to update second time"` - ); - expect(rulesClientParams.getUserName).toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(2); - expect(taskManager.schedule).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); + expect(taskManager.bulkEnableDisable).toHaveBeenCalledWith(['task-123'], true); }); - test('throws error when failing to schedule task', async () => { - taskManager.schedule.mockRejectedValueOnce(new Error('Fail to schedule')); - + test('throws error when enabling task fails', async () => { + taskManager.bulkEnableDisable.mockRejectedValueOnce(new Error('Failed to enable task')); await expect(rulesClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Fail to schedule"` + `"Failed to enable task"` ); - expect(rulesClientParams.getUserName).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); }); - test('enables a rule if conflict errors received when scheduling a task', async () => { - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - ...existingRuleWithoutApiKey, - attributes: { - ...existingRuleWithoutApiKey.attributes, - enabled: true, - apiKey: null, - apiKeyOwner: null, - updatedBy: 'elastic', - }, + test('schedules task when scheduledTaskId is defined but task with that ID does not', async () => { + taskManager.schedule.mockResolvedValueOnce({ + id: '1', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, }); - taskManager.schedule.mockRejectedValueOnce( - Object.assign(new Error('Conflict!'), { statusCode: 409 }) - ); - + taskManager.get.mockRejectedValueOnce(new Error('Failed to get task!')); await rulesClient.enable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); - expect(unsecuredSavedObjectsClient.create).not.toBeCalledWith('api_key_pending_invalidation'); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - name: 'name', - schedule: { interval: '10s' }, - alertTypeId: 'myType', + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(2); + expect(taskManager.bulkEnableDisable).not.toHaveBeenCalled(); + expect(taskManager.schedule).toHaveBeenCalledWith({ + id: '1', + taskType: `alerting:myType`, + params: { + alertId: '1', + spaceId: 'default', consumer: 'myApp', - enabled: true, - meta: { - versionApiKeyLastmodified: kibanaVersion, - }, - updatedAt: '2019-02-12T21:01:22.479Z', - updatedBy: 'elastic', - apiKey: 'MTIzOmFiYw==', - apiKeyOwner: 'elastic', - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - executionStatus: { - status: 'pending', - lastDuration: 0, - lastExecutionDate: '2019-02-12T21:01:22.479Z', - error: null, - warning: null, - }, }, - { - version: '123', - } - ); + schedule: { + interval: '10s', + }, + enabled: true, + state: { + alertInstances: {}, + alertTypeState: {}, + previousStartedAt: null, + }, + scope: ['alerting'], + }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenNthCalledWith(2, 'alert', '1', { + scheduledTaskId: '1', + }); + }); + + test('schedules task when scheduledTaskId is not defined', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ + ...existingRule, + attributes: { ...existingRule.attributes, scheduledTaskId: null }, + }); + taskManager.schedule.mockResolvedValueOnce({ + id: '1', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + await rulesClient.enable({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(2); + expect(taskManager.bulkEnableDisable).not.toHaveBeenCalled(); expect(taskManager.schedule).toHaveBeenCalledWith({ id: '1', taskType: `alerting:myType`, @@ -524,6 +520,7 @@ describe('enable()', () => { schedule: { interval: '10s', }, + enabled: true, state: { alertInstances: {}, alertTypeState: {}, @@ -531,7 +528,81 @@ describe('enable()', () => { }, scope: ['alerting'], }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { + expect(unsecuredSavedObjectsClient.update).toHaveBeenNthCalledWith(2, 'alert', '1', { + scheduledTaskId: '1', + }); + }); + + test('throws error when scheduling task fails', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ + ...existingRule, + attributes: { ...existingRule.attributes, scheduledTaskId: null }, + }); + taskManager.schedule.mockRejectedValueOnce(new Error('Fail to schedule')); + await expect(rulesClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Fail to schedule"` + ); + expect(rulesClientParams.getUserName).toHaveBeenCalled(); + expect(taskManager.bulkEnableDisable).not.toHaveBeenCalled(); + expect(taskManager.schedule).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); + }); + + test('succeeds if conflict errors received when scheduling a task', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ + ...existingRule, + attributes: { ...existingRule.attributes, scheduledTaskId: null }, + }); + taskManager.schedule.mockRejectedValueOnce( + Object.assign(new Error('Conflict!'), { statusCode: 409 }) + ); + await rulesClient.enable({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(2); + expect(taskManager.bulkEnableDisable).not.toHaveBeenCalled(); + expect(taskManager.schedule).toHaveBeenCalled(); + }); + + test('throws error when update after scheduling task fails', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ + ...existingRule, + attributes: { ...existingRule.attributes, scheduledTaskId: null }, + }); + taskManager.schedule.mockResolvedValueOnce({ + id: '1', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + ...existingRule, + attributes: { + ...existingRule.attributes, + enabled: true, + }, + }); + unsecuredSavedObjectsClient.update.mockRejectedValueOnce( + new Error('Fail to update after scheduling task') + ); + + await expect(rulesClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Fail to update after scheduling task"` + ); + expect(rulesClientParams.getUserName).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(2); + expect(taskManager.schedule).toHaveBeenCalled(); + expect(taskManager.bulkEnableDisable).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenNthCalledWith(2, 'alert', '1', { scheduledTaskId: '1', }); }); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts index cf525f59e9448c..f5525820da6dc1 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts @@ -145,6 +145,7 @@ const aggregateResults = { _source: { rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', + name: 'rule-name', }, event: { outcome: 'success', @@ -248,7 +249,7 @@ const aggregateResults = { _id: 'a4wIZX8B8TGQpG7Xwpnz', _score: 1.0, _source: { - rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef' }, + rule: { id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', name: 'rule-name' }, event: { outcome: 'success', }, @@ -377,6 +378,7 @@ describe('getExecutionLogForRule()', () => { timed_out: false, schedule_delay_ms: 3126, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', + rule_name: 'rule-name', }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -398,6 +400,7 @@ describe('getExecutionLogForRule()', () => { timed_out: false, schedule_delay_ms: 3345, rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', + rule_name: 'rule-name', }, ], }); diff --git a/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts b/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts index 2416e95c38f648..b71cc0276dc646 100644 --- a/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts +++ b/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts @@ -141,9 +141,9 @@ async function enable(success: boolean) { return expectConflict(success, err); } - // a successful enable call makes 2 calls to update, so that's 3 total, - // 1 with conflict + 2 on success - expectSuccess(success, 3); + // a successful enable call makes 1 call to update, so with + // conflict, we would expect 1 on conflict, 1 on success + expectSuccess(success, 2); } async function disable(success: boolean) { diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index a90c274ce2e867..4ce85f54c3dc52 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -6,7 +6,6 @@ */ import sinon from 'sinon'; -import { schema } from '@kbn/config-schema'; import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; import { RuleExecutorOptions, @@ -1447,105 +1446,6 @@ describe('Task Runner', () => { expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); - test('validates params before running the rule type', async () => { - const taskRunner = new TaskRunner( - { - ...ruleType, - validate: { - params: schema.object({ - param1: schema.string(), - }), - }, - }, - { - ...mockedTaskInstance, - params: { - ...mockedTaskInstance.params, - spaceId: 'foo', - }, - }, - taskRunnerFactoryInitializerParams, - inMemoryMetrics - ); - expect(AlertingEventLogger).toHaveBeenCalled(); - - rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); - const runnerResult = await taskRunner.run(); - expect(runnerResult).toEqual(generateRunnerResult({ successRatio: 0 })); - const loggerCall = logger.error.mock.calls[0][0]; - const loggerMeta = logger.error.mock.calls[0][1]; - const loggerCallPrefix = (loggerCall as string).split('-'); - expect(loggerCallPrefix[0].trim()).toMatchInlineSnapshot( - `"Executing Rule foo:test:1 has resulted in Error: params invalid: [param1]: expected value of type [string] but got [undefined]"` - ); - expect(loggerMeta?.tags).toEqual(['test', '1', 'rule-run-failed']); - expect(loggerMeta?.error?.stack_trace).toBeDefined(); - expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); - }); - - test('uses API key when provided', async () => { - const taskRunner = new TaskRunner( - ruleType, - mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics - ); - expect(AlertingEventLogger).toHaveBeenCalled(); - - rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); - - await taskRunner.run(); - expect(taskRunnerFactoryInitializerParams.getRulesClientWithRequest).toHaveBeenCalledWith( - expect.objectContaining({ - headers: { - // base64 encoded "123:abc" - authorization: 'ApiKey MTIzOmFiYw==', - }, - }) - ); - const [request] = taskRunnerFactoryInitializerParams.getRulesClientWithRequest.mock.calls[0]; - - expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith( - request, - '/' - ); - expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); - }); - - test(`doesn't use API key when not provided`, async () => { - const taskRunner = new TaskRunner( - ruleType, - mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics - ); - expect(AlertingEventLogger).toHaveBeenCalled(); - - rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ - ...SAVED_OBJECT, - attributes: { enabled: true }, - }); - - await taskRunner.run(); - - expect(taskRunnerFactoryInitializerParams.getRulesClientWithRequest).toHaveBeenCalledWith( - expect.objectContaining({ - headers: {}, - }) - ); - - const [request] = taskRunnerFactoryInitializerParams.getRulesClientWithRequest.mock.calls[0]; - - expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith( - request, - '/' - ); - expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); - }); - test('rescheduled the rule if the schedule has update during a task run', async () => { const taskRunner = new TaskRunner( ruleType, @@ -1618,95 +1518,8 @@ describe('Task Runner', () => { expect(logger.error).toBeCalledTimes(1); }); - test('recovers gracefully when the Alert Task Runner throws an exception when fetching the encrypted attributes', async () => { - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockImplementation(() => { - throw new Error(GENERIC_ERROR_MESSAGE); - }); - - const taskRunner = new TaskRunner( - ruleType, - mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics - ); - expect(AlertingEventLogger).toHaveBeenCalled(); - - rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - - const runnerResult = await taskRunner.run(); - - expect(runnerResult).toEqual(generateRunnerResult({ successRatio: 0 })); - - testAlertingEventLogCalls({ - setRuleName: false, - status: 'error', - errorReason: 'decrypt', - executionStatus: 'not-reached', - }); - - expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); - }); - - test('recovers gracefully when the Alert Task Runner throws an exception when license is higher than supported', async () => { - ruleTypeRegistry.ensureRuleTypeEnabled.mockImplementation(() => { - throw new Error(GENERIC_ERROR_MESSAGE); - }); - - const taskRunner = new TaskRunner( - ruleType, - mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics - ); - expect(AlertingEventLogger).toHaveBeenCalled(); - - rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); - - const runnerResult = await taskRunner.run(); - - expect(runnerResult).toEqual(generateRunnerResult({ successRatio: 0 })); - - testAlertingEventLogCalls({ - status: 'error', - errorReason: 'license', - executionStatus: 'not-reached', - }); - - expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); - }); - - test('recovers gracefully when the Alert Task Runner throws an exception when getting internal Services', async () => { - taskRunnerFactoryInitializerParams.getRulesClientWithRequest.mockImplementation(() => { - throw new Error(GENERIC_ERROR_MESSAGE); - }); - - const taskRunner = new TaskRunner( - ruleType, - mockedTaskInstance, - taskRunnerFactoryInitializerParams, - inMemoryMetrics - ); - expect(AlertingEventLogger).toHaveBeenCalled(); - - rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); - - const runnerResult = await taskRunner.run(); - - expect(runnerResult).toEqual(generateRunnerResult({ successRatio: 0 })); - - testAlertingEventLogCalls({ - setRuleName: false, - status: 'error', - errorReason: 'unknown', - executionStatus: 'not-reached', - }); - - expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); - }); - - test('recovers gracefully when the Alert Task Runner throws an exception when fetching attributes', async () => { + test('recovers gracefully when the Alert Task Runner throws an exception when loading rule to prepare for run', async () => { + // used in loadRule() which is called in prepareToRun() rulesClient.get.mockImplementation(() => { throw new Error(GENERIC_ERROR_MESSAGE); }); @@ -2381,42 +2194,6 @@ describe('Task Runner', () => { expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); - test('successfully bails on execution if the rule is disabled', async () => { - const state = { - ...mockedTaskInstance.state, - previousStartedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), - }; - const taskRunner = new TaskRunner( - ruleType, - { - ...mockedTaskInstance, - state, - }, - taskRunnerFactoryInitializerParams, - inMemoryMetrics - ); - expect(AlertingEventLogger).toHaveBeenCalled(); - - rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - ...SAVED_OBJECT, - attributes: { ...SAVED_OBJECT.attributes, enabled: false }, - }); - const runnerResult = await taskRunner.run(); - expect(runnerResult.state.previousStartedAt?.toISOString()).toBe(state.previousStartedAt); - expect(runnerResult.schedule).toStrictEqual(mockedTaskInstance.schedule); - - testAlertingEventLogCalls({ - setRuleName: false, - status: 'error', - errorReason: 'disabled', - errorMessage: `Rule failed to execute because rule ran after it was disabled.`, - executionStatus: 'not-reached', - }); - - expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); - }); - test('successfully stores successful runs', async () => { const taskRunner = new TaskRunner( ruleType, diff --git a/x-pack/plugins/apm/ftr_e2e/cypress.config.ts b/x-pack/plugins/apm/ftr_e2e/cypress.config.ts new file mode 100644 index 00000000000000..7a92b84ac36bd4 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress.config.ts @@ -0,0 +1,36 @@ +/* + * 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 { defineConfig } from 'cypress'; +import { plugin } from './cypress/plugins'; + +module.exports = defineConfig({ + fileServerFolder: './cypress', + fixturesFolder: './cypress/fixtures', + screenshotsFolder: './cypress/screenshots', + videosFolder: './cypress/videos', + requestTimeout: 10000, + responseTimeout: 40000, + defaultCommandTimeout: 30000, + execTimeout: 120000, + pageLoadTimeout: 120000, + viewportHeight: 900, + viewportWidth: 1440, + video: false, + screenshotOnRunFailure: false, + e2e: { + // We've imported your old cypress plugins here. + // You may want to clean this up later by importing these. + setupNodeEvents(on, config) { + plugin(on, config); + }, + baseUrl: 'http://localhost:5601', + supportFile: './cypress/support/e2e.ts', + specPattern: './cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', + experimentalSessionAndOrigin: false, + }, +}); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress.json b/x-pack/plugins/apm/ftr_e2e/cypress.json deleted file mode 100644 index 848a10efed6682..00000000000000 --- a/x-pack/plugins/apm/ftr_e2e/cypress.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "fileServerFolder": "./cypress", - "fixturesFolder": "./cypress/fixtures", - "integrationFolder": "./cypress/integration", - "pluginsFile": "./cypress/plugins/index.ts", - "screenshotsFolder": "./cypress/screenshots", - "supportFile": "./cypress/support/index.ts", - "videosFolder": "./cypress/videos", - "requestTimeout": 10000, - "responseTimeout": 40000, - "defaultCommandTimeout": 30000, - "execTimeout": 120000, - "pageLoadTimeout": 120000, - "viewportHeight": 900, - "viewportWidth": 1440, - "video": false, - "screenshotOnRunFailure": false, - "experimentalSessionAndOrigin": true -} diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/feature_flag/comparison.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/feature_flag/comparison.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/feature_flag/comparison.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/feature_flag/comparison.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/infrastructure/generate_data.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/infrastructure/generate_data.ts similarity index 72% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/infrastructure/generate_data.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/infrastructure/generate_data.ts index 52cf6b988f1a1d..dde70238377a77 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/infrastructure/generate_data.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/infrastructure/generate_data.ts @@ -9,21 +9,29 @@ import { apm, timerange } from '@kbn/apm-synthtrace'; export function generateData({ from, to }: { from: number; to: number }) { const range = timerange(from, to); const serviceRunsInContainerInstance = apm - .service('synth-go', 'production', 'go') + .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) .instance('instance-a'); const serviceInstance = apm - .service('synth-java', 'production', 'java') + .service({ + name: 'synth-java', + environment: 'production', + agentName: 'java', + }) .instance('instance-b'); const serviceNoInfraDataInstance = apm - .service('synth-node', 'production', 'node') + .service({ + name: 'synth-node', + environment: 'production', + agentName: 'node', + }) .instance('instance-b'); return range.interval('1m').generator((timestamp) => { return [ serviceRunsInContainerInstance - .transaction('GET /apple 🍎') + .transaction({ transactionName: 'GET /apple 🍎' }) .defaults({ 'container.id': 'foo', 'host.hostname': 'bar', @@ -33,7 +41,7 @@ export function generateData({ from, to }: { from: number; to: number }) { .duration(1000) .success(), serviceInstance - .transaction('GET /banana 🍌') + .transaction({ transactionName: 'GET /banana 🍌' }) .defaults({ 'host.hostname': 'bar', }) @@ -41,7 +49,7 @@ export function generateData({ from, to }: { from: number; to: number }) { .duration(1000) .success(), serviceNoInfraDataInstance - .transaction('GET /banana 🍌') + .transaction({ transactionName: 'GET /banana 🍌' }) .timestamp(timestamp) .duration(1000) .success(), diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/infrastructure/infrastructure_page.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/infrastructure/infrastructure_page.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/infrastructure/infrastructure_page.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/infrastructure/infrastructure_page.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/integration_settings/integration_policy.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/integration_settings/integration_policy.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/integration_settings/integration_policy.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/integration_settings/integration_policy.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/no_data_screen.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/no_data_screen.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/no_data_screen.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/no_data_screen.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/rules/error_count.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/rules/error_count.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/rules/error_count.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/rules/error_count.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/agent_configurations.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/agent_configurations.cy.ts similarity index 90% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/agent_configurations.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/agent_configurations.cy.ts index 23154492c9f441..5be39b4f082dc1 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/agent_configurations.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/agent_configurations.cy.ts @@ -29,12 +29,20 @@ function generateData({ const range = timerange(from, to); const service1 = apm - .service(serviceName, 'production', 'java') + .service({ + name: serviceName, + environment: 'production', + agentName: 'java', + }) .instance('service-1-prod-1') .podId('service-1-prod-1-pod'); const service2 = apm - .service(serviceName, 'development', 'nodejs') + .service({ + name: serviceName, + environment: 'development', + agentName: 'nodejs', + }) .instance('opbeans-node-prod-1'); return range @@ -42,12 +50,12 @@ function generateData({ .rate(1) .generator((timestamp, index) => [ service1 - .transaction('GET /apple 🍎 ') + .transaction({ transactionName: 'GET /apple 🍎 ' }) .timestamp(timestamp) .duration(1000) .success(), service2 - .transaction('GET /banana 🍌') + .transaction({ transactionName: 'GET /banana 🍌' }) .timestamp(timestamp) .duration(500) .success(), diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/custom_links.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/custom_links.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/custom_links.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/custom_links.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/deep_links.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/deep_links.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/dependencies.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/dependencies.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/dependencies.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/dependencies.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/error_details.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/error_details.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/errors_page.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/errors_page.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/errors_page.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/errors_page.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/generate_data.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/generate_data.ts similarity index 70% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/generate_data.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/generate_data.ts index 56978f03123a81..8f432305f2ba9f 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/generate_data.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/generate_data.ts @@ -10,12 +10,20 @@ export function generateData({ from, to }: { from: number; to: number }) { const range = timerange(from, to); const opbeansJava = apm - .service('opbeans-java', 'production', 'java') + .service({ + name: 'opbeans-java', + environment: 'production', + agentName: 'java', + }) .instance('opbeans-java-prod-1') .podId('opbeans-java-prod-1-pod'); const opbeansNode = apm - .service('opbeans-node', 'production', 'nodejs') + .service({ + name: 'opbeans-node', + environment: 'production', + agentName: 'nodejs', + }) .instance('opbeans-node-prod-1'); return range @@ -23,17 +31,17 @@ export function generateData({ from, to }: { from: number; to: number }) { .rate(1) .generator((timestamp, index) => [ opbeansJava - .transaction('GET /apple 🍎 ') + .transaction({ transactionName: 'GET /apple 🍎 ' }) .timestamp(timestamp) .duration(1000) .success() .errors( opbeansJava - .error(`Error ${index}`, `exception ${index}`) + .error({ message: `Error ${index}`, type: `exception ${index}` }) .timestamp(timestamp) ), opbeansNode - .transaction('GET /banana 🍌') + .transaction({ transactionName: 'GET /banana 🍌' }) .timestamp(timestamp) .duration(500) .success(), @@ -52,7 +60,11 @@ export function generateErrors({ const range = timerange(from, to); const opbeansJava = apm - .service('opbeans-java', 'production', 'java') + .service({ + name: 'opbeans-java', + environment: 'production', + agentName: 'java', + }) .instance('opbeans-java-prod-1') .podId('opbeans-java-prod-1-pod'); @@ -61,7 +73,7 @@ export function generateErrors({ .rate(1) .generator((timestamp, index) => [ opbeansJava - .transaction('GET /apple 🍎 ') + .transaction({ transactionName: 'GET /apple 🍎 ' }) .timestamp(timestamp) .duration(1000) .success() @@ -70,7 +82,7 @@ export function generateErrors({ .fill(0) .map((_, idx) => { return opbeansJava - .error(`Error ${idx}`, `exception ${idx}`) + .error({ message: `Error ${idx}`, type: `exception ${idx}` }) .timestamp(timestamp); }) ), diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/home.cy.ts similarity index 95% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/home.cy.ts index be9acfd38ab0ce..2ee2f4f019b12a 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/home.cy.ts @@ -31,6 +31,10 @@ describe('Home page', () => { to: new Date(end).getTime(), }) ); + + cy.updateAdvancedSettings({ + 'observability:enableComparisonByDefault': true, + }); }); after(() => { diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/generate_data.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/generate_data.ts similarity index 81% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/generate_data.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/generate_data.ts index e3cdf7e8bbce88..3fd41b8a06fd06 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/generate_data.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/generate_data.ts @@ -19,7 +19,11 @@ export function generateMultipleServicesData({ .fill(0) .map((_, idx) => apm - .service(`${idx}`, 'production', 'nodejs') + .service({ + name: `${idx}`, + environment: 'production', + agentName: 'nodejs', + }) .instance('opbeans-node-prod-1') ); @@ -29,7 +33,7 @@ export function generateMultipleServicesData({ .generator((timestamp, index) => services.map((service) => service - .transaction('GET /foo') + .transaction({ transactionName: 'GET /foo' }) .timestamp(timestamp) .duration(500) .success() diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/generate_data.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/header_filters/generate_data.ts similarity index 72% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/generate_data.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/header_filters/generate_data.ts index 243f1df257a4f9..6467768f75e28c 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/generate_data.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/header_filters/generate_data.ts @@ -18,12 +18,20 @@ export function generateData({ const range = timerange(from, to); const service1 = apm - .service(specialServiceName, 'production', 'java') + .service({ + name: specialServiceName, + environment: 'production', + agentName: 'java', + }) .instance('service-1-prod-1') .podId('service-1-prod-1-pod'); const opbeansNode = apm - .service('opbeans-node', 'production', 'nodejs') + .service({ + name: 'opbeans-node', + environment: 'production', + agentName: 'nodejs', + }) .instance('opbeans-node-prod-1'); return range @@ -31,12 +39,12 @@ export function generateData({ .rate(1) .generator((timestamp) => [ service1 - .transaction('GET /apple 🍎 ') + .transaction({ transactionName: 'GET /apple 🍎 ' }) .timestamp(timestamp) .duration(1000) .success(), opbeansNode - .transaction('GET /banana 🍌') + .transaction({ transactionName: 'GET /banana 🍌' }) .timestamp(timestamp) .duration(500) .success(), diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/header_filters.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/header_filters/header_filters.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/header_filters.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/header_filters/header_filters.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/service_inventory.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/service_inventory.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/aws_lambda/aws_lamba.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/aws_lambda/aws_lamba.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/aws_lambda/aws_lamba.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/aws_lambda/aws_lamba.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/aws_lambda/generate_data.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/aws_lambda/generate_data.ts similarity index 85% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/aws_lambda/generate_data.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/aws_lambda/generate_data.ts index bbd7553d1fa335..81d6aabf38165c 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/aws_lambda/generate_data.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/aws_lambda/generate_data.ts @@ -18,7 +18,11 @@ const dataConfig = { export function generateData({ start, end }: { start: number; end: number }) { const { rate, transaction, serviceName } = dataConfig; const instance = apm - .service(serviceName, 'production', 'python') + .service({ + name: serviceName, + environment: 'production', + agentName: 'python', + }) .instance('instance-a'); const traceEvents = timerange(start, end) @@ -26,7 +30,7 @@ export function generateData({ start, end }: { start: number; end: number }) { .rate(rate) .generator((timestamp) => instance - .transaction(transaction.name) + .transaction({ transactionName: transaction.name }) .defaults({ 'service.runtime.name': 'AWS_Lambda_python3.8', 'faas.coldstart': true, diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/errors_table.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/errors_table.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/errors_table.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/errors_table.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/header_filters.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/header_filters.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/instances_table.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/instances_table.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/service_overview.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/service_overview.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/time_comparison.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/time_comparison.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/time_comparison.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/time_comparison.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/generate_span_links_data.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/generate_span_links_data.ts similarity index 86% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/generate_span_links_data.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/generate_span_links_data.ts index 9fd2cfe6eab0b4..d623ea664bc53c 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/generate_span_links_data.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/generate_span_links_data.ts @@ -10,7 +10,11 @@ import { SpanLink } from '../../../../../typings/es_schemas/raw/fields/span_link function getProducerInternalOnly() { const producerInternalOnlyInstance = apm - .service('producer-internal-only', 'production', 'go') + .service({ + name: 'producer-internal-only', + environment: 'production', + agentName: 'go', + }) .instance('instance a'); const events = timerange( @@ -21,13 +25,17 @@ function getProducerInternalOnly() { .rate(1) .generator((timestamp) => { return producerInternalOnlyInstance - .transaction(`Transaction A`) + .transaction({ transactionName: `Transaction A` }) .timestamp(timestamp) .duration(1000) .success() .children( producerInternalOnlyInstance - .span(`Span A`, 'external', 'http') + .span({ + spanName: `Span A`, + spanType: 'external', + spanSubtype: 'http', + }) .timestamp(timestamp + 50) .duration(100) .success() @@ -61,7 +69,11 @@ function getProducerInternalOnly() { function getProducerExternalOnly() { const producerExternalOnlyInstance = apm - .service('producer-external-only', 'production', 'java') + .service({ + name: 'producer-external-only', + environment: 'production', + agentName: 'java', + }) .instance('instance b'); const events = timerange( @@ -72,13 +84,17 @@ function getProducerExternalOnly() { .rate(1) .generator((timestamp) => { return producerExternalOnlyInstance - .transaction(`Transaction B`) + .transaction({ transactionName: `Transaction B` }) .timestamp(timestamp) .duration(1000) .success() .children( producerExternalOnlyInstance - .span(`Span B`, 'external', 'http') + .span({ + spanName: `Span B`, + spanType: 'external', + spanSubtype: 'http', + }) .defaults({ 'span.links': [ { trace: { id: 'trace#1' }, span: { id: 'span#1' } }, @@ -88,7 +104,11 @@ function getProducerExternalOnly() { .duration(100) .success(), producerExternalOnlyInstance - .span(`Span B.1`, 'external', 'http') + .span({ + spanName: `Span B.1`, + spanType: 'external', + spanSubtype: 'http', + }) .timestamp(timestamp + 50) .duration(100) .success() @@ -132,7 +152,11 @@ function getProducerConsumer({ producerInternalOnlySpanASpanLink?: SpanLink; }) { const producerConsumerInstance = apm - .service('producer-consumer', 'production', 'ruby') + .service({ + name: 'producer-consumer', + environment: 'production', + agentName: 'ruby', + }) .instance('instance c'); const events = timerange( @@ -143,7 +167,7 @@ function getProducerConsumer({ .rate(1) .generator((timestamp) => { return producerConsumerInstance - .transaction(`Transaction C`) + .transaction({ transactionName: `Transaction C` }) .defaults({ 'span.links': producerInternalOnlySpanASpanLink ? [producerInternalOnlySpanASpanLink] @@ -154,7 +178,11 @@ function getProducerConsumer({ .success() .children( producerConsumerInstance - .span(`Span C`, 'external', 'http') + .span({ + spanName: `Span C`, + spanType: 'external', + spanSubtype: 'http', + }) .timestamp(timestamp + 50) .duration(100) .success() @@ -209,7 +237,11 @@ function getConsumerMultiple({ producerConsumerTransactionCSpanLink?: SpanLink; }) { const consumerMultipleInstance = apm - .service('consumer-multiple', 'production', 'nodejs') + .service({ + name: 'consumer-multiple', + environment: 'production', + agentName: 'nodejs', + }) .instance('instance d'); const events = timerange( @@ -220,7 +252,7 @@ function getConsumerMultiple({ .rate(1) .generator((timestamp) => { return consumerMultipleInstance - .transaction(`Transaction D`) + .transaction({ transactionName: `Transaction D` }) .defaults({ 'span.links': producerInternalOnlySpanASpanLink && producerConsumerSpanCSpanLink @@ -235,7 +267,11 @@ function getConsumerMultiple({ .success() .children( consumerMultipleInstance - .span(`Span E`, 'external', 'http') + .span({ + spanName: `Span E`, + spanType: 'external', + spanSubtype: 'http', + }) .defaults({ 'span.links': producerExternalOnlySpanBSpanLink && diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/span_links.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/span_links.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/span_links.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/span_links.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/transaction_details.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/transaction_details.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/transaction_details.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/transaction_details.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transactions_overview/transactions_overview.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transactions_overview/transactions_overview.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/tutorial/tutorial.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/tutorial/tutorial.cy.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/tutorial/tutorial.spec.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/tutorial/tutorial.cy.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/fixtures/synthtrace/opbeans.ts b/x-pack/plugins/apm/ftr_e2e/cypress/fixtures/synthtrace/opbeans.ts index 1be9873d25c4f5..bf8802c39f9f80 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/fixtures/synthtrace/opbeans.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/fixtures/synthtrace/opbeans.ts @@ -10,50 +10,70 @@ export function opbeans({ from, to }: { from: number; to: number }) { const range = timerange(from, to); const opbeansJava = apm - .service('opbeans-java', 'production', 'java') + .service({ + name: 'opbeans-java', + environment: 'production', + agentName: 'java', + }) .instance('opbeans-java-prod-1') .podId('opbeans-java-prod-1-pod'); const opbeansNode = apm - .service('opbeans-node', 'production', 'nodejs') + .service({ + name: 'opbeans-node', + environment: 'production', + agentName: 'nodejs', + }) .instance('opbeans-node-prod-1'); - const opbeansRum = apm.browser( - 'opbeans-rum', - 'production', - apm.getChromeUserAgentDefaults() - ); + const opbeansRum = apm.browser({ + serviceName: 'opbeans-rum', + environment: 'production', + userAgent: apm.getChromeUserAgentDefaults(), + }); return range .interval('1s') .rate(1) .generator((timestamp) => [ opbeansJava - .transaction('GET /api/product') + .transaction({ transactionName: 'GET /api/product' }) .timestamp(timestamp) .duration(1000) .success() .errors( - opbeansJava.error('[MockError] Foo', `Exception`).timestamp(timestamp) + opbeansJava + .error({ message: '[MockError] Foo', type: `Exception` }) + .timestamp(timestamp) ) .children( opbeansJava - .span('SELECT * FROM product', 'db', 'postgresql') + .span({ + spanName: 'SELECT * FROM product', + spanType: 'db', + spanSubtype: 'postgresql', + }) .timestamp(timestamp) .duration(50) .success() .destination('postgresql') ), opbeansNode - .transaction('GET /api/product/:id') + .transaction({ transactionName: 'GET /api/product/:id' }) .timestamp(timestamp) .duration(500) .success(), opbeansNode - .transaction('Worker job', 'Worker') + .transaction({ + transactionName: 'Worker job', + transactionType: 'Worker', + }) .timestamp(timestamp) .duration(1000) .success(), - opbeansRum.transaction('/').timestamp(timestamp).duration(1000), + opbeansRum + .transaction({ transactionName: '/' }) + .timestamp(timestamp) + .duration(1000), ]); } diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/plugins/index.ts b/x-pack/plugins/apm/ftr_e2e/cypress/plugins/index.ts index 2cc7595ce67313..8adaad0b71c631 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/plugins/index.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/plugins/index.ts @@ -29,7 +29,7 @@ import { createEsClientForTesting } from '@kbn/test'; * @type {Cypress.PluginConfig} */ -const plugin: Cypress.PluginConfig = (on, config) => { +export const plugin: Cypress.PluginConfig = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config @@ -66,5 +66,3 @@ const plugin: Cypress.PluginConfig = (on, config) => { }, }); }; - -module.exports = plugin; diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts index 37182e328ebf37..692926d3049ca9 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts @@ -21,25 +21,26 @@ Cypress.Commands.add('loginAsEditorUser', () => { Cypress.Commands.add( 'loginAs', ({ username, password }: { username: string; password: string }) => { - cy.log(`Calling 'loginAs'`); - cy.session([username, password], () => { - cy.log(`Logging in as ${username}`); - const kibanaUrl = Cypress.env('KIBANA_URL'); - cy.request({ - log: false, - method: 'POST', - url: `${kibanaUrl}/internal/security/login`, - body: { - providerType: 'basic', - providerName: 'basic', - currentURL: `${kibanaUrl}/login`, - params: { username, password }, - }, - headers: { - 'kbn-xsrf': 'e2e_test', - }, - }); + // cy.session(username, () => { + const kibanaUrl = Cypress.env('KIBANA_URL'); + cy.log(`Logging in as ${username} on ${kibanaUrl}`); + cy.visit('/'); + cy.request({ + log: true, + method: 'POST', + url: `${kibanaUrl}/internal/security/login`, + body: { + providerType: 'basic', + providerName: 'basic', + currentURL: `${kibanaUrl}/login`, + params: { username, password }, + }, + headers: { + 'kbn-xsrf': 'e2e_test', + }, + // }); }); + cy.visit('/'); } ); diff --git a/x-pack/test/visual_regression/page_objects.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/e2e.ts similarity index 65% rename from x-pack/test/visual_regression/page_objects.ts rename to x-pack/plugins/apm/ftr_e2e/cypress/support/e2e.ts index c8b0c9050dbb9c..93daa0bc7ed2ac 100644 --- a/x-pack/test/visual_regression/page_objects.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/e2e.ts @@ -5,6 +5,9 @@ * 2.0. */ -import { pageObjects } from '../functional/page_objects'; +Cypress.on('uncaught:exception', (err, runnable) => { + return false; +}); -export { pageObjects }; +import './commands'; +// import './output_command_timings'; diff --git a/x-pack/plugins/apm/ftr_e2e/cypress_test_runner.ts b/x-pack/plugins/apm/ftr_e2e/cypress_test_runner.ts index 86316fe7ef8c82..9736a695e81c73 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress_test_runner.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress_test_runner.ts @@ -58,10 +58,9 @@ export async function cypressTestRunner({ getService }: FtrProviderContext) { ...cypressCliArgs, project: cypressProjectPath, config: { - baseUrl: kibanaUrl, - requestTimeout: 10000, - responseTimeout: 60000, - defaultCommandTimeout: 15000, + e2e: { + baseUrl: kibanaUrl, + }, }, env: { KIBANA_URL: kibanaUrl, diff --git a/x-pack/plugins/cases/common/api/cases/suggest_user_profiles.ts b/x-pack/plugins/cases/common/api/cases/suggest_user_profiles.ts deleted file mode 100644 index 75cd1f9cb9f94f..00000000000000 --- a/x-pack/plugins/cases/common/api/cases/suggest_user_profiles.ts +++ /dev/null @@ -1,18 +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 * as rt from 'io-ts'; - -export const SuggestUserProfilesRequestRt = rt.intersection([ - rt.type({ - name: rt.string, - owners: rt.array(rt.string), - }), - rt.partial({ size: rt.number }), -]); - -export type SuggestUserProfilesRequest = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/api/user.ts b/x-pack/plugins/cases/common/api/user.ts index 2696ad60a4568e..63280d230b7778 100644 --- a/x-pack/plugins/cases/common/api/user.ts +++ b/x-pack/plugins/cases/common/api/user.ts @@ -7,11 +7,14 @@ import * as rt from 'io-ts'; -export const UserRT = rt.type({ - email: rt.union([rt.undefined, rt.null, rt.string]), - full_name: rt.union([rt.undefined, rt.null, rt.string]), - username: rt.union([rt.undefined, rt.null, rt.string]), -}); +export const UserRT = rt.intersection([ + rt.type({ + email: rt.union([rt.undefined, rt.null, rt.string]), + full_name: rt.union([rt.undefined, rt.null, rt.string]), + username: rt.union([rt.undefined, rt.null, rt.string]), + }), + rt.partial({ profile_uid: rt.string }), +]); export const UsersRt = rt.array(UserRT); diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index 9e85d6e4cbf7a3..a0488094283d0b 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -103,14 +103,17 @@ export const GENERAL_CASES_OWNER = APP_ID; export const OWNER_INFO = { [SECURITY_SOLUTION_OWNER]: { + appId: 'securitySolutionUI', label: 'Security', iconType: 'logoSecurity', }, [OBSERVABILITY_OWNER]: { + appId: 'observability-overview', label: 'Observability', iconType: 'logoObservability', }, [GENERAL_CASES_OWNER]: { + appId: 'management', label: 'Stack', iconType: 'casesApp', }, @@ -163,3 +166,8 @@ export const PUSH_CASES_CAPABILITY = 'push_cases' as const; */ export const DEFAULT_USER_SIZE = 10; + +/** + * Delays + */ +export const SEARCH_DEBOUNCE_MS = 500; diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 9fef8ae47b3b0b..2ae40c2e339617 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -103,6 +103,7 @@ export interface FilterOptions { severity: CaseSeverityWithAll; status: CaseStatusWithAllStatus; tags: string[]; + assignees: string[]; reporters: User[]; owner: string[]; } @@ -162,7 +163,7 @@ export interface FieldMappings { export type UpdateKey = keyof Pick< CasePatchRequest, - 'connector' | 'description' | 'status' | 'tags' | 'title' | 'settings' | 'severity' + 'connector' | 'description' | 'status' | 'tags' | 'title' | 'settings' | 'severity' | 'assignees' >; export interface UpdateByKey { diff --git a/x-pack/plugins/cases/public/common/mock/index.ts b/x-pack/plugins/cases/public/common/mock/index.ts index add4c1c206dd4c..c607eb2985af83 100644 --- a/x-pack/plugins/cases/public/common/mock/index.ts +++ b/x-pack/plugins/cases/public/common/mock/index.ts @@ -6,3 +6,4 @@ */ export * from './test_providers'; +export * from './permissions'; diff --git a/x-pack/plugins/cases/public/common/mock/permissions.ts b/x-pack/plugins/cases/public/common/mock/permissions.ts new file mode 100644 index 00000000000000..1166dbed8ca881 --- /dev/null +++ b/x-pack/plugins/cases/public/common/mock/permissions.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CasesCapabilities, CasesPermissions } from '../../containers/types'; + +export const allCasesPermissions = () => buildCasesPermissions(); +export const noCasesPermissions = () => + buildCasesPermissions({ read: false, create: false, update: false, delete: false, push: false }); +export const readCasesPermissions = () => + buildCasesPermissions({ read: true, create: false, update: false, delete: false, push: false }); +export const noCreateCasesPermissions = () => buildCasesPermissions({ create: false }); +export const noUpdateCasesPermissions = () => buildCasesPermissions({ update: false }); +export const noPushCasesPermissions = () => buildCasesPermissions({ push: false }); +export const noDeleteCasesPermissions = () => buildCasesPermissions({ delete: false }); +export const writeCasesPermissions = () => buildCasesPermissions({ read: false }); + +export const buildCasesPermissions = (overrides: Partial> = {}) => { + const create = overrides.create ?? true; + const read = overrides.read ?? true; + const update = overrides.update ?? true; + const deletePermissions = overrides.delete ?? true; + const push = overrides.push ?? true; + const all = create && read && update && deletePermissions && push; + + return { + all, + create, + read, + update, + delete: deletePermissions, + push, + }; +}; + +export const allCasesCapabilities = () => buildCasesCapabilities(); +export const noCasesCapabilities = () => + buildCasesCapabilities({ + create_cases: false, + read_cases: false, + update_cases: false, + delete_cases: false, + push_cases: false, + }); +export const readCasesCapabilities = () => + buildCasesCapabilities({ + create_cases: false, + update_cases: false, + delete_cases: false, + push_cases: false, + }); +export const writeCasesCapabilities = () => { + return buildCasesCapabilities({ + read_cases: false, + }); +}; + +export const buildCasesCapabilities = (overrides?: Partial) => { + return { + create_cases: overrides?.create_cases ?? true, + read_cases: overrides?.read_cases ?? true, + update_cases: overrides?.update_cases ?? true, + delete_cases: overrides?.delete_cases ?? true, + push_cases: overrides?.push_cases ?? true, + }; +}; diff --git a/x-pack/plugins/cases/public/common/mock/test_providers.tsx b/x-pack/plugins/cases/public/common/mock/test_providers.tsx index dceb8fd0f30a71..a180fb942bd158 100644 --- a/x-pack/plugins/cases/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/cases/public/common/mock/test_providers.tsx @@ -5,6 +5,8 @@ * 2.0. */ +/* eslint-disable no-console */ + import React from 'react'; import { euiDarkVars } from '@kbn/ui-theme'; import { I18nProvider } from '@kbn/i18n-react'; @@ -14,7 +16,7 @@ import { render as reactRender, RenderOptions, RenderResult } from '@testing-lib import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; -import { CasesCapabilities, CasesFeatures, CasesPermissions } from '../../../common/ui/types'; +import { CasesFeatures, CasesPermissions } from '../../../common/ui/types'; import { CasesProvider } from '../../components/cases_context'; import { createKibanaContextProviderMock, @@ -25,6 +27,7 @@ import { StartServices } from '../../types'; import { ReleasePhase } from '../../components/types'; import { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry'; import { PersistableStateAttachmentTypeRegistry } from '../../client/attachment_framework/persistable_state_registry'; +import { allCasesPermissions } from './permissions'; interface TestProviderProps { children: React.ReactNode; @@ -56,6 +59,11 @@ const TestProvidersComponent: React.FC = ({ retry: false, }, }, + logger: { + log: console.log, + warn: console.warn, + error: () => {}, + }, }); return ( @@ -98,69 +106,17 @@ export const testQueryClient = new QueryClient({ retry: false, }, }, + /** + * React query prints the errors in the console even though + * all tests are passings. We turn them off for testing. + */ + logger: { + log: console.log, + warn: console.warn, + error: () => {}, + }, }); -export const allCasesPermissions = () => buildCasesPermissions(); -export const noCasesPermissions = () => - buildCasesPermissions({ read: false, create: false, update: false, delete: false, push: false }); -export const readCasesPermissions = () => - buildCasesPermissions({ read: true, create: false, update: false, delete: false, push: false }); -export const noCreateCasesPermissions = () => buildCasesPermissions({ create: false }); -export const noUpdateCasesPermissions = () => buildCasesPermissions({ update: false }); -export const noPushCasesPermissions = () => buildCasesPermissions({ push: false }); -export const noDeleteCasesPermissions = () => buildCasesPermissions({ delete: false }); -export const writeCasesPermissions = () => buildCasesPermissions({ read: false }); - -export const buildCasesPermissions = (overrides: Partial> = {}) => { - const create = overrides.create ?? true; - const read = overrides.read ?? true; - const update = overrides.update ?? true; - const deletePermissions = overrides.delete ?? true; - const push = overrides.push ?? true; - const all = create && read && update && deletePermissions && push; - - return { - all, - create, - read, - update, - delete: deletePermissions, - push, - }; -}; - -export const allCasesCapabilities = () => buildCasesCapabilities(); -export const noCasesCapabilities = () => - buildCasesCapabilities({ - create_cases: false, - read_cases: false, - update_cases: false, - delete_cases: false, - push_cases: false, - }); -export const readCasesCapabilities = () => - buildCasesCapabilities({ - create_cases: false, - update_cases: false, - delete_cases: false, - push_cases: false, - }); -export const writeCasesCapabilities = () => { - return buildCasesCapabilities({ - read_cases: false, - }); -}; - -export const buildCasesCapabilities = (overrides?: Partial) => { - return { - create_cases: overrides?.create_cases ?? true, - read_cases: overrides?.read_cases ?? true, - update_cases: overrides?.update_cases ?? true, - delete_cases: overrides?.delete_cases ?? true, - push_cases: overrides?.push_cases ?? true, - }; -}; - export const createAppMockRenderer = ({ features, owner = [SECURITY_SOLUTION_OWNER], @@ -176,6 +132,11 @@ export const createAppMockRenderer = ({ retry: false, }, }, + logger: { + log: console.log, + warn: console.warn, + error: () => {}, + }, }); const AppWrapper: React.FC<{ children: React.ReactElement }> = ({ children }) => ( diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx index c764df4d6661d7..e8661387e1e649 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx @@ -6,20 +6,25 @@ */ import { renderHook } from '@testing-library/react-hooks'; -import { useToasts } from './lib/kibana'; +import { useKibana, useToasts } from './lib/kibana'; import { AppMockRenderer, createAppMockRenderer, TestProviders } from './mock'; import { CaseToastSuccessContent, useCasesToast } from './use_cases_toast'; import { alertComment, basicComment, mockCase } from '../containers/mock'; import React from 'react'; import userEvent from '@testing-library/user-event'; import { SupportedCaseAttachment } from '../types'; +import { getByTestId } from '@testing-library/dom'; +import { OWNER_INFO } from '../../common/constants'; jest.mock('./lib/kibana'); const useToastsMock = useToasts as jest.Mock; +const useKibanaMock = useKibana as jest.Mocked; describe('Use cases toast hook', () => { const successMock = jest.fn(); + const getUrlForApp = jest.fn().mockReturnValue(`/app/cases/${mockCase.id}`); + const navigateToUrl = jest.fn(); function validateTitle(title: string) { const mockParams = successMock.mock.calls[0][0]; @@ -35,6 +40,14 @@ describe('Use cases toast hook', () => { expect(el).toHaveTextContent(content); } + function navigateToCase() { + const mockParams = successMock.mock.calls[0][0]; + const el = document.createElement('div'); + mockParams.text(el); + const button = getByTestId(el, 'toaster-content-case-view-link'); + userEvent.click(button); + } + useToastsMock.mockImplementation(() => { return { addSuccess: successMock, @@ -42,7 +55,12 @@ describe('Use cases toast hook', () => { }); beforeEach(() => { - successMock.mockClear(); + jest.clearAllMocks(); + useKibanaMock().services.application = { + ...useKibanaMock().services.application, + getUrlForApp, + navigateToUrl, + }; }); describe('Toast hook', () => { @@ -119,6 +137,7 @@ describe('Use cases toast hook', () => { validateTitle('Another horrible breach!! has been updated'); }); }); + describe('Toast content', () => { let appMockRender: AppMockRenderer; const onViewCaseClick = jest.fn(); @@ -192,4 +211,52 @@ describe('Use cases toast hook', () => { expect(onViewCaseClick).toHaveBeenCalled(); }); }); + + describe('Toast navigation', () => { + const tests = Object.entries(OWNER_INFO).map(([owner, ownerInfo]) => [owner, ownerInfo.appId]); + + it.each(tests)('should navigate correctly with owner %s and appId %s', (owner, appId) => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + + result.current.showSuccessAttach({ + theCase: { ...mockCase, owner }, + title: 'Custom title', + }); + + navigateToCase(); + + expect(getUrlForApp).toHaveBeenCalledWith(appId, { + deepLinkId: 'cases', + path: '/mock-id', + }); + + expect(navigateToUrl).toHaveBeenCalledWith('/app/cases/mock-id'); + }); + + it('navigates to the current app if the owner is invalid', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + + result.current.showSuccessAttach({ + theCase: { ...mockCase, owner: 'in-valid' }, + title: 'Custom title', + }); + + navigateToCase(); + + expect(getUrlForApp).toHaveBeenCalledWith('testAppId', { + deepLinkId: 'cases', + path: '/mock-id', + }); + }); + }); }); diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.tsx index 5e88831144b6b8..7d445a4edffac9 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.tsx @@ -10,8 +10,8 @@ import React from 'react'; import styled from 'styled-components'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { Case, CommentType } from '../../common'; -import { useToasts } from './lib/kibana'; -import { useCaseViewNavigation } from './navigation'; +import { useKibana, useToasts } from './lib/kibana'; +import { generateCaseViewPath } from './navigation'; import { CaseAttachmentsWithoutOwner } from '../types'; import { CASE_ALERT_SUCCESS_SYNC_TEXT, @@ -19,6 +19,8 @@ import { CASE_SUCCESS_TOAST, VIEW_CASE, } from './translations'; +import { OWNER_INFO } from '../../common/constants'; +import { useCasesContext } from '../components/cases_context/use_cases_context'; const LINE_CLAMP = 3; const Title = styled.span` @@ -93,8 +95,12 @@ function getToastContent({ return undefined; } +const isValidOwner = (owner: string): owner is keyof typeof OWNER_INFO => + Object.keys(OWNER_INFO).includes(owner); + export const useCasesToast = () => { - const { navigateToCaseView } = useCaseViewNavigation(); + const { appId } = useCasesContext(); + const { getUrlForApp, navigateToUrl } = useKibana().services.application; const toasts = useToasts(); @@ -110,11 +116,19 @@ export const useCasesToast = () => { title?: string; content?: string; }) => { + const appIdToNavigateTo = isValidOwner(theCase.owner) + ? OWNER_INFO[theCase.owner].appId + : appId; + + const url = getUrlForApp(appIdToNavigateTo, { + deepLinkId: 'cases', + path: generateCaseViewPath({ detailName: theCase.id }), + }); + const onViewCaseClick = () => { - navigateToCaseView({ - detailName: theCase.id, - }); + navigateToUrl(url); }; + const renderTitle = getToastTitle({ theCase, title, attachments }); const renderContent = getToastContent({ theCase, content, attachments }); diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx index d8aedbc10bd3de..5a77eb7155b86e 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx @@ -37,12 +37,14 @@ import { registerConnectorsToMockActionRegistry } from '../../common/mock/regist import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock'; import { waitForComponentToUpdate } from '../../common/test_utils'; import { useCreateAttachments } from '../../containers/use_create_attachments'; -import { useGetReporters } from '../../containers/use_get_reporters'; import { useGetCasesMetrics } from '../../containers/use_get_cases_metrics'; import { useGetConnectors } from '../../containers/configure/use_connectors'; import { useGetTags } from '../../containers/use_get_tags'; import { useUpdateCase } from '../../containers/use_update_case'; import { useGetCases } from '../../containers/use_get_cases'; +import { useGetCurrentUserProfile } from '../../containers/user_profiles/use_get_current_user_profile'; +import { userProfiles, userProfilesMap } from '../../containers/user_profiles/api.mock'; +import { useBulkGetUserProfiles } from '../../containers/user_profiles/use_bulk_get_user_profiles'; jest.mock('../../containers/use_create_attachments'); jest.mock('../../containers/use_bulk_update_case'); @@ -52,7 +54,8 @@ jest.mock('../../containers/use_get_cases_status'); jest.mock('../../containers/use_get_cases_metrics'); jest.mock('../../containers/use_get_action_license'); jest.mock('../../containers/use_get_tags'); -jest.mock('../../containers/use_get_reporters'); +jest.mock('../../containers/user_profiles/use_get_current_user_profile'); +jest.mock('../../containers/user_profiles/use_bulk_get_user_profiles'); jest.mock('../../containers/configure/use_connectors'); jest.mock('../../common/lib/kibana'); jest.mock('../../common/navigation/hooks'); @@ -67,7 +70,8 @@ const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; const useGetCasesMetricsMock = useGetCasesMetrics as jest.Mock; const useUpdateCasesMock = useUpdateCases as jest.Mock; const useGetTagsMock = useGetTags as jest.Mock; -const useGetReportersMock = useGetReporters as jest.Mock; +const useGetCurrentUserProfileMock = useGetCurrentUserProfile as jest.Mock; +const useBulkGetUserProfilesMock = useBulkGetUserProfiles as jest.Mock; const useKibanaMock = useKibana as jest.MockedFunction; const useGetConnectorsMock = useGetConnectors as jest.Mock; const useCreateAttachmentsMock = useCreateAttachments as jest.Mock; @@ -145,6 +149,8 @@ describe('AllCasesListGeneric', () => { handleIsLoading: jest.fn(), isLoadingCases: [], isSelectorView: false, + userProfiles: new Map(), + currentUserProfile: undefined, }; let appMockRenderer: AppMockRenderer; @@ -164,13 +170,8 @@ describe('AllCasesListGeneric', () => { useGetCasesStatusMock.mockReturnValue(defaultCasesStatus); useGetCasesMetricsMock.mockReturnValue(defaultCasesMetrics); useGetTagsMock.mockReturnValue({ data: ['coke', 'pepsi'], refetch: jest.fn() }); - useGetReportersMock.mockReturnValue({ - reporters: ['casetester'], - respReporters: [{ username: 'casetester' }], - isLoading: true, - isError: false, - fetchReporters: jest.fn(), - }); + useGetCurrentUserProfileMock.mockReturnValue({ data: userProfiles[0], isLoading: false }); + useBulkGetUserProfilesMock.mockReturnValue({ data: userProfilesMap }); useGetConnectorsMock.mockImplementation(() => ({ data: connectorsMock, isLoading: false })); useUpdateCaseMock.mockReturnValue({ updateCaseProperty }); mockKibana(); @@ -194,9 +195,9 @@ describe('AllCasesListGeneric', () => { expect( wrapper.find(`span[data-test-subj="case-table-column-tags-coke"]`).first().prop('title') ).toEqual(useGetCasesMockState.data.cases[0].tags[0]); - expect(wrapper.find(`[data-test-subj="case-table-column-createdBy"]`).first().text()).toEqual( - 'LK' - ); + expect( + wrapper.find(`[data-test-subj="case-user-profile-avatar-damaged_raccoon"]`).first().text() + ).toEqual('DR'); expect( wrapper .find(`[data-test-subj="case-table-column-createdAt"]`) @@ -215,20 +216,17 @@ describe('AllCasesListGeneric', () => { }); }); - it('should show a tooltip with the reporter username when hover over the reporter avatar', async () => { + it("should show a tooltip with the assignee's email when hover over the assignee avatar", async () => { const result = render( ); - userEvent.hover(result.queryAllByTestId('case-table-column-createdBy')[0]); + userEvent.hover(result.queryAllByTestId('case-user-profile-avatar-damaged_raccoon')[0]); await waitFor(() => { - expect(result.getByTestId('case-table-column-createdBy-tooltip')).toBeTruthy(); - expect(result.getByTestId('case-table-column-createdBy-tooltip').textContent).toEqual( - 'lknope' - ); + expect(result.getByText('damaged_raccoon@elastic.co')).toBeInTheDocument(); }); }); @@ -263,6 +261,7 @@ describe('AllCasesListGeneric', () => { title: null, totalComment: null, totalAlerts: null, + assignees: [], }, ], }, @@ -588,7 +587,7 @@ describe('AllCasesListGeneric', () => { wrapper.find('[data-test-subj="cases-table-row-select-1"]').first().simulate('click'); await waitFor(() => { expect(onRowClick).toHaveBeenCalledWith({ - assignees: [], + assignees: [{ uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0' }], closedAt: null, closedBy: null, comments: [], @@ -899,4 +898,108 @@ describe('AllCasesListGeneric', () => { expect(alertCounts.length).toBeGreaterThan(0); }); + + describe('Solutions', () => { + it('should set the owner to all available solutions when deselecting all solutions', async () => { + const { getByTestId } = appMockRenderer.render( + + + + ); + + expect(useGetCasesMock).toHaveBeenCalledWith({ + filterOptions: { + search: '', + searchFields: [], + severity: 'all', + reporters: [], + status: 'all', + tags: [], + assignees: [], + owner: ['securitySolution', 'observability'], + }, + queryParams: { page: 1, perPage: 5, sortField: 'createdAt', sortOrder: 'desc' }, + }); + + userEvent.click(getByTestId('options-filter-popover-button-Solution')); + + await waitForEuiPopoverOpen(); + + userEvent.click( + getByTestId(`options-filter-popover-item-${SECURITY_SOLUTION_OWNER}`), + undefined, + { + skipPointerEventsCheck: true, + } + ); + + expect(useGetCasesMock).toBeCalledWith({ + filterOptions: { + search: '', + searchFields: [], + severity: 'all', + reporters: [], + status: 'all', + tags: [], + assignees: [], + owner: ['securitySolution'], + }, + queryParams: { page: 1, perPage: 5, sortField: 'createdAt', sortOrder: 'desc' }, + }); + + userEvent.click( + getByTestId(`options-filter-popover-item-${SECURITY_SOLUTION_OWNER}`), + undefined, + { + skipPointerEventsCheck: true, + } + ); + + expect(useGetCasesMock).toHaveBeenLastCalledWith({ + filterOptions: { + search: '', + searchFields: [], + severity: 'all', + reporters: [], + status: 'all', + tags: [], + assignees: [], + owner: ['securitySolution', 'observability'], + }, + queryParams: { page: 1, perPage: 5, sortField: 'createdAt', sortOrder: 'desc' }, + }); + }); + + it('should hide the solutions filter if the owner is provided', async () => { + const { queryByTestId } = appMockRenderer.render( + + + + ); + + expect(queryByTestId('options-filter-popover-button-Solution')).toBeFalsy(); + }); + + it('should call useGetCases with the correct owner on initial render', async () => { + appMockRenderer.render( + + + + ); + + expect(useGetCasesMock).toHaveBeenCalledWith({ + filterOptions: { + search: '', + searchFields: [], + severity: 'all', + reporters: [], + status: 'all', + tags: [], + assignees: [], + owner: ['securitySolution'], + }, + queryParams: { page: 1, perPage: 5, sortField: 'createdAt', sortOrder: 'desc' }, + }); + }); + }); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx index 3c056ccf996dd7..0bc5e9b14b29a7 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx @@ -10,6 +10,7 @@ import { EuiProgress, EuiBasicTable, EuiTableSelectionType } from '@elastic/eui' import { difference, head, isEmpty } from 'lodash/fp'; import styled, { css } from 'styled-components'; +import { useQueryClient } from '@tanstack/react-query'; import { Case, CaseStatusWithAllStatus, @@ -35,6 +36,13 @@ import { initialData, useGetCases, } from '../../containers/use_get_cases'; +import { useBulkGetUserProfiles } from '../../containers/user_profiles/use_bulk_get_user_profiles'; +import { useGetCurrentUserProfile } from '../../containers/user_profiles/use_get_current_user_profile'; +import { + USER_PROFILES_BULK_GET_CACHE_KEY, + USER_PROFILES_CACHE_KEY, +} from '../../containers/constants'; +import { getAllPermissionsExceptFrom } from '../../utils/permissions'; const ProgressLoader = styled(EuiProgress)` ${({ $isShow }: { $isShow: boolean }) => @@ -62,7 +70,7 @@ export interface AllCasesListProps { export const AllCasesList = React.memo( ({ hiddenStatuses = [], isSelectorView = false, onRowClick, doRefresh }) => { const { owner, permissions } = useCasesContext(); - const availableSolutions = useAvailableCasesOwners(); + const availableSolutions = useAvailableCasesOwners(getAllPermissionsExceptFrom('delete')); const [refresh, setRefresh] = useState(0); const hasOwner = !!owner.length; @@ -70,7 +78,7 @@ export const AllCasesList = React.memo( const firstAvailableStatus = head(difference(caseStatuses, hiddenStatuses)); const initialFilterOptions = { ...(!isEmpty(hiddenStatuses) && firstAvailableStatus && { status: firstAvailableStatus }), - owner: hasOwner ? owner : [], + owner: hasOwner ? owner : availableSolutions, }; const [filterOptions, setFilterOptions] = useState({ ...DEFAULT_FILTER_OPTIONS, @@ -78,6 +86,7 @@ export const AllCasesList = React.memo( }); const [queryParams, setQueryParams] = useState(DEFAULT_QUERY_PARAMS); const [selectedCases, setSelectedCases] = useState([]); + const queryClient = useQueryClient(); const { data = initialData, @@ -88,6 +97,26 @@ export const AllCasesList = React.memo( queryParams, }); + const assigneesFromCases = useMemo(() => { + return data.cases.reduce>((acc, caseInfo) => { + if (!caseInfo) { + return acc; + } + + for (const assignee of caseInfo.assignees) { + acc.add(assignee.uid); + } + return acc; + }, new Set()); + }, [data.cases]); + + const { data: userProfiles } = useBulkGetUserProfiles({ + uids: Array.from(assigneesFromCases), + }); + + const { data: currentUserProfile, isLoading: isLoadingCurrentUserProfile } = + useGetCurrentUserProfile(); + const { data: connectors = [] } = useGetConnectors(); const sorting = useMemo( @@ -118,6 +147,8 @@ export const AllCasesList = React.memo( deselectCases(); if (dataRefresh) { refetchCases(); + queryClient.refetchQueries([USER_PROFILES_CACHE_KEY, USER_PROFILES_BULK_GET_CACHE_KEY]); + setRefresh((currRefresh: number) => currRefresh + 1); } if (doRefresh) { @@ -127,7 +158,7 @@ export const AllCasesList = React.memo( filterRefetch.current(); } }, - [deselectCases, doRefresh, refetchCases] + [deselectCases, doRefresh, queryClient, refetchCases] ); const tableOnChangeCallback = useCallback( @@ -179,10 +210,28 @@ export const AllCasesList = React.memo( setFilterOptions((prevFilterOptions) => ({ ...prevFilterOptions, ...newFilterOptions, + /** + * If the user selects and deselects all solutions + * then the owner is set to an empty array. This results in fetching all cases the user has access to including + * the ones with read access. We want to show only the cases the user has full access to. + * For that reason we fallback to availableSolutions if the owner is empty. + * + * If the consumer of cases has passed an owner we fallback to the provided owner + */ + ...(newFilterOptions.owner != null && !hasOwner + ? { + owner: + newFilterOptions.owner.length === 0 ? availableSolutions : newFilterOptions.owner, + } + : newFilterOptions.owner != null && hasOwner + ? { + owner: newFilterOptions.owner.length === 0 ? owner : newFilterOptions.owner, + } + : {}), })); refreshCases(false); }, - [deselectCases, setFilterOptions, refreshCases, setQueryParams] + [deselectCases, refreshCases, hasOwner, availableSolutions, owner] ); /** @@ -193,6 +242,8 @@ export const AllCasesList = React.memo( const columns = useCasesColumns({ filterStatus: filterOptions.status ?? StatusAll, + userProfiles: userProfiles ?? new Map(), + currentUserProfile, handleIsLoading, refreshCases, isSelectorView, @@ -245,6 +296,7 @@ export const AllCasesList = React.memo( initial={{ search: filterOptions.search, searchFields: filterOptions.searchFields, + assignees: filterOptions.assignees, reporters: filterOptions.reporters, tags: filterOptions.tags, status: filterOptions.status, @@ -255,6 +307,8 @@ export const AllCasesList = React.memo( hiddenStatuses={hiddenStatuses} displayCreateCaseButton={isSelectorView} onCreateCasePressed={onRowClick} + isLoading={isLoadingCurrentUserProfile} + currentUserProfile={currentUserProfile} /> { + let appMockRender: AppMockRenderer; + let defaultProps: AssigneesFilterPopoverProps; + + beforeEach(() => { + jest.clearAllMocks(); + + appMockRender = createAppMockRenderer(); + + defaultProps = { + currentUserProfile: undefined, + selectedAssignees: [], + isLoading: false, + onSelectionChange: jest.fn(), + }; + }); + + it('calls onSelectionChange when 1 user is selected', async () => { + const onSelectionChange = jest.fn(); + const props = { ...defaultProps, onSelectionChange }; + appMockRender.render(); + + await waitFor(() => { + userEvent.click(screen.getByTestId('options-filter-popover-button-assignees')); + expect(screen.getByPlaceholderText('Search users')).toBeInTheDocument(); + }); + await waitForEuiPopoverOpen(); + + fireEvent.change(screen.getByPlaceholderText('Search users'), { target: { value: 'dingo' } }); + userEvent.click(screen.getByText('wet_dingo@elastic.co')); + + expect(onSelectionChange.mock.calls[0][0]).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object {}, + "enabled": true, + "uid": "u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0", + "user": Object { + "email": "wet_dingo@elastic.co", + "full_name": "Wet Dingo", + "username": "wet_dingo", + }, + }, + ] + `); + }); + + it('calls onSelectionChange with a single user when different users are selected', async () => { + const onSelectionChange = jest.fn(); + const props = { ...defaultProps, onSelectionChange }; + appMockRender.render(); + + await waitFor(() => { + userEvent.click(screen.getByTestId('options-filter-popover-button-assignees')); + expect(screen.getByText('wet_dingo@elastic.co')); + }); + + await waitForEuiPopoverOpen(); + + fireEvent.change(screen.getByPlaceholderText('Search users'), { target: { value: 'dingo' } }); + userEvent.click(screen.getByText('wet_dingo@elastic.co')); + userEvent.click(screen.getByText('damaged_raccoon@elastic.co')); + + expect(onSelectionChange.mock.calls[0][0]).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object {}, + "enabled": true, + "uid": "u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0", + "user": Object { + "email": "wet_dingo@elastic.co", + "full_name": "Wet Dingo", + "username": "wet_dingo", + }, + }, + ] + `); + expect(onSelectionChange.mock.calls[1][0]).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object {}, + "enabled": true, + "uid": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0", + "user": Object { + "email": "damaged_raccoon@elastic.co", + "full_name": "Damaged Raccoon", + "username": "damaged_raccoon", + }, + }, + ] + `); + }); + + it('does not show the assigned users total if there are no assigned users', async () => { + appMockRender.render(); + + await waitFor(() => { + userEvent.click(screen.getByTestId('options-filter-popover-button-assignees')); + expect(screen.getByText('Damaged Raccoon')).toBeInTheDocument(); + }); + + await waitForEuiPopoverOpen(); + + expect(screen.queryByText('assignee')).not.toBeInTheDocument(); + }); + + it('shows the 1 assigned total when the users are passed in', async () => { + const props = { + ...defaultProps, + selectedAssignees: [userProfiles[0]], + }; + appMockRender.render(); + + await waitFor(async () => { + userEvent.click(screen.getByTestId('options-filter-popover-button-assignees')); + expect(screen.getByText('1 assignee filtered')).toBeInTheDocument(); + }); + + await waitForEuiPopoverOpen(); + + expect(screen.getByText('Damaged Raccoon')).toBeInTheDocument(); + }); + + it('shows three users when initially rendered', async () => { + appMockRender.render(); + + await waitFor(() => { + userEvent.click(screen.getByTestId('options-filter-popover-button-assignees')); + expect(screen.getByText('Wet Dingo')).toBeInTheDocument(); + }); + await waitForEuiPopoverOpen(); + + expect(screen.getByText('Damaged Raccoon')).toBeInTheDocument(); + expect(screen.getByText('Physical Dinosaur')).toBeInTheDocument(); + }); + + it('shows the users sorted alphabetically with the current user at the front', async () => { + const props = { + ...defaultProps, + currentUserProfile: userProfiles[2], + }; + + appMockRender.render(); + + await waitFor(() => { + userEvent.click(screen.getByTestId('options-filter-popover-button-assignees')); + expect(screen.getByText('Wet Dingo')).toBeInTheDocument(); + }); + await waitForEuiPopoverOpen(); + + const assignees = screen.getAllByRole('option'); + expect(within(assignees[0]).getByText('Wet Dingo')).toBeInTheDocument(); + expect(within(assignees[1]).getByText('Damaged Raccoon')).toBeInTheDocument(); + expect(within(assignees[2]).getByText('Physical Dinosaur')).toBeInTheDocument(); + }); + + it('does not show the number of filters', async () => { + appMockRender.render(); + + await waitFor(() => { + userEvent.click(screen.getByTestId('options-filter-popover-button-assignees')); + expect(screen.getByText('Wet Dingo')).toBeInTheDocument(); + }); + await waitForEuiPopoverOpen(); + + expect(screen.queryByText('3')).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/all_cases/assignees_filter.tsx b/x-pack/plugins/cases/public/components/all_cases/assignees_filter.tsx new file mode 100644 index 00000000000000..6ffa18cfa80734 --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/assignees_filter.tsx @@ -0,0 +1,126 @@ +/* + * 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 { EuiFilterButton } from '@elastic/eui'; +import { UserProfilesPopover, UserProfileWithAvatar } from '@kbn/user-profile-components'; +import { isEmpty } from 'lodash'; +import React, { useCallback, useMemo, useState } from 'react'; +import { useSuggestUserProfiles } from '../../containers/user_profiles/use_suggest_user_profiles'; +import { useAvailableCasesOwners } from '../app/use_available_owners'; +import { useCasesContext } from '../cases_context/use_cases_context'; +import { CurrentUserProfile } from '../types'; +import { EmptyMessage } from '../user_profiles/empty_message'; +import { NoMatches } from '../user_profiles/no_matches'; +import { SelectedStatusMessage } from '../user_profiles/selected_status_message'; +import { bringCurrentUserToFrontAndSort } from '../user_profiles/sort'; +import * as i18n from './translations'; + +export interface AssigneesFilterPopoverProps { + selectedAssignees: UserProfileWithAvatar[]; + currentUserProfile: CurrentUserProfile; + isLoading: boolean; + onSelectionChange: (users: UserProfileWithAvatar[]) => void; +} + +const AssigneesFilterPopoverComponent: React.FC = ({ + selectedAssignees, + currentUserProfile, + isLoading, + onSelectionChange, +}) => { + const { owner: owners } = useCasesContext(); + const hasOwners = owners.length > 0; + const availableOwners = useAvailableCasesOwners(['read']); + const [searchTerm, setSearchTerm] = useState(''); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const togglePopover = useCallback(() => setIsPopoverOpen((value) => !value), []); + + const onChange = useCallback( + (users: UserProfileWithAvatar[]) => { + const sortedUsers = bringCurrentUserToFrontAndSort(currentUserProfile, users); + onSelectionChange(sortedUsers ?? []); + }, + [currentUserProfile, onSelectionChange] + ); + + const selectedStatusMessage = useCallback( + (selectedCount: number) => ( + + ), + [] + ); + + const onSearchChange = useCallback((term: string) => { + setSearchTerm(term); + + if (!isEmpty(term)) { + setIsUserTyping(true); + } + }, []); + + const [isUserTyping, setIsUserTyping] = useState(false); + + const onDebounce = useCallback(() => setIsUserTyping(false), []); + + const { data: userProfiles, isLoading: isLoadingSuggest } = useSuggestUserProfiles({ + name: searchTerm, + owners: hasOwners ? owners : availableOwners, + onDebounce, + }); + + const searchResultProfiles = useMemo( + () => bringCurrentUserToFrontAndSort(currentUserProfile, userProfiles), + [userProfiles, currentUserProfile] + ); + + const isLoadingData = isLoading || isLoadingSuggest; + + return ( + 0} + numActiveFilters={selectedAssignees.length} + aria-label={i18n.FILTER_ASSIGNEES_ARIA_LABEL} + > + {i18n.ASSIGNEES} + + } + selectableProps={{ + onChange, + onSearchChange, + selectedStatusMessage, + options: searchResultProfiles, + selectedOptions: selectedAssignees, + isLoading: isLoadingData || isUserTyping, + height: 'full', + searchPlaceholder: i18n.SEARCH_USERS, + clearButtonLabel: i18n.CLEAR_FILTERS, + emptyMessage: , + noMatchesMessage: !isUserTyping && !isLoadingData ? : , + singleSelection: false, + }} + /> + ); +}; +AssigneesFilterPopoverComponent.displayName = 'AssigneesFilterPopover'; + +export const AssigneesFilterPopover = React.memo(AssigneesFilterPopoverComponent); diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx index 0929f8971cf06e..be5b8ace2e2b69 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx @@ -7,7 +7,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { - EuiAvatar, EuiBadgeGroup, EuiBadge, EuiButton, @@ -24,6 +23,7 @@ import { import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import styled from 'styled-components'; +import { UserProfileWithAvatar } from '@kbn/user-profile-components'; import { Case, DeleteCase, UpdateByKey } from '../../../common/ui/types'; import { CaseStatuses, ActionConnector, CaseSeverity } from '../../../common/api'; import { OWNER_INFO } from '../../../common/constants'; @@ -44,6 +44,11 @@ import { useCasesFeatures } from '../cases_context/use_cases_features'; import { severities } from '../severity/config'; import { useUpdateCase } from '../../containers/use_update_case'; import { useCasesContext } from '../cases_context/use_cases_context'; +import { UserToolTip } from '../user_profiles/user_tooltip'; +import { CaseUserAvatar } from '../user_profiles/user_avatar'; +import { useAssignees } from '../../containers/user_profiles/use_assignees'; +import { getUsernameDataTestSubj } from '../user_profiles/data_test_subject'; +import { CurrentUserProfile } from '../types'; export type CasesColumns = | EuiTableActionsColumnType @@ -57,8 +62,45 @@ const MediumShadeText = styled.p` const renderStringField = (field: string, dataTestSubj: string) => field != null ? {field} : getEmptyTagValue(); +const AssigneesColumn: React.FC<{ + assignees: Case['assignees']; + userProfiles: Map; + currentUserProfile: CurrentUserProfile; +}> = ({ assignees, userProfiles, currentUserProfile }) => { + const { allAssignees } = useAssignees({ + caseAssignees: assignees, + userProfiles, + currentUserProfile, + }); + + if (allAssignees.length <= 0) { + return getEmptyTagValue(); + } + + return ( + + {allAssignees.map((assignee) => { + const dataTestSubjName = getUsernameDataTestSubj(assignee); + return ( + + + + + + ); + })} + + ); +}; +AssigneesColumn.displayName = 'AssigneesColumn'; export interface GetCasesColumn { filterStatus: string; + userProfiles: Map; + currentUserProfile: CurrentUserProfile; handleIsLoading: (a: boolean) => void; refreshCases?: (a?: boolean) => void; isSelectorView: boolean; @@ -69,6 +111,8 @@ export interface GetCasesColumn { } export const useCasesColumns = ({ filterStatus, + userProfiles, + currentUserProfile, handleIsLoading, refreshCases, isSelectorView, @@ -173,27 +217,15 @@ export const useCasesColumns = ({ }, }, { - field: 'createdBy', - name: i18n.REPORTER, - render: (createdBy: Case['createdBy']) => { - if (createdBy != null) { - return ( - - - - ); - } - return getEmptyTagValue(); - }, + field: 'assignees', + name: i18n.ASSIGNEES, + render: (assignees: Case['assignees']) => ( + + ), }, { field: 'tags', diff --git a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx index 8e5263a31ff3df..38f0b9d53b1d16 100644 --- a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx @@ -16,15 +16,16 @@ import { noCreateCasesPermissions, TestProviders, } from '../../common/mock'; -import { useGetReporters } from '../../containers/use_get_reporters'; import { useGetActionLicense } from '../../containers/use_get_action_license'; import { casesStatus, connectorsMock, useGetCasesMockState } from '../../containers/mock'; import { useGetCasesStatus } from '../../containers/use_get_cases_status'; import { useGetConnectors } from '../../containers/configure/use_connectors'; import { useGetTags } from '../../containers/use_get_tags'; import { useGetCases } from '../../containers/use_get_cases'; +import { useGetCurrentUserProfile } from '../../containers/user_profiles/use_get_current_user_profile'; +import { userProfiles, userProfilesMap } from '../../containers/user_profiles/api.mock'; +import { useBulkGetUserProfiles } from '../../containers/user_profiles/use_bulk_get_user_profiles'; -jest.mock('../../containers/use_get_reporters'); jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/use_get_action_license', () => { return { @@ -35,11 +36,15 @@ jest.mock('../../containers/configure/use_connectors'); jest.mock('../../containers/api'); jest.mock('../../containers/use_get_cases'); jest.mock('../../containers/use_get_cases_status'); +jest.mock('../../containers/user_profiles/use_get_current_user_profile'); +jest.mock('../../containers/user_profiles/use_bulk_get_user_profiles'); const useGetConnectorsMock = useGetConnectors as jest.Mock; const useGetCasesMock = useGetCases as jest.Mock; const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; const useGetActionLicenseMock = useGetActionLicense as jest.Mock; +const useGetCurrentUserProfileMock = useGetCurrentUserProfile as jest.Mock; +const useBulkGetUserProfilesMock = useBulkGetUserProfiles as jest.Mock; describe('AllCases', () => { const refetchCases = jest.fn(); @@ -71,17 +76,13 @@ describe('AllCases', () => { beforeAll(() => { (useGetTags as jest.Mock).mockReturnValue({ data: ['coke', 'pepsi'], refetch: jest.fn() }); - (useGetReporters as jest.Mock).mockReturnValue({ - reporters: ['casetester'], - respReporters: [{ username: 'casetester' }], - isLoading: true, - isError: false, - fetchReporters: jest.fn(), - }); useGetConnectorsMock.mockImplementation(() => ({ data: connectorsMock, isLoading: false })); useGetCasesStatusMock.mockReturnValue(defaultCasesStatus); useGetActionLicenseMock.mockReturnValue(defaultActionLicense); useGetCasesMock.mockReturnValue(defaultGetCases); + + useGetCurrentUserProfileMock.mockReturnValue({ data: userProfiles[0], isLoading: false }); + useBulkGetUserProfilesMock.mockReturnValue({ data: userProfilesMap }); }); let appMockRender: AppMockRenderer; diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx index 4c477e1b4a581e..2d6f4076f6cf30 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx @@ -7,22 +7,23 @@ import React from 'react'; import { mount } from 'enzyme'; +import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; import { CaseStatuses } from '../../../common/api'; import { OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock'; -import { useGetReporters } from '../../containers/use_get_reporters'; import { DEFAULT_FILTER_OPTIONS } from '../../containers/use_get_cases'; import { CasesTableFilters } from './table_filters'; import { useGetTags } from '../../containers/use_get_tags'; +import { useSuggestUserProfiles } from '../../containers/user_profiles/use_suggest_user_profiles'; +import { userProfiles } from '../../containers/user_profiles/api.mock'; -jest.mock('../../containers/use_get_reporters'); jest.mock('../../containers/use_get_tags'); +jest.mock('../../containers/user_profiles/use_suggest_user_profiles'); const onFilterChanged = jest.fn(); -const fetchReporters = jest.fn(); const refetch = jest.fn(); const setFilterRefetch = jest.fn(); @@ -34,6 +35,8 @@ const props = { initial: DEFAULT_FILTER_OPTIONS, setFilterRefetch, availableSolutions: [], + isLoading: false, + currentUserProfile: undefined, }; describe('CasesTableFilters ', () => { @@ -42,13 +45,7 @@ describe('CasesTableFilters ', () => { appMockRender = createAppMockRenderer(); jest.clearAllMocks(); (useGetTags as jest.Mock).mockReturnValue({ data: ['coke', 'pepsi'], refetch }); - (useGetReporters as jest.Mock).mockReturnValue({ - reporters: ['casetester'], - respReporters: [{ username: 'casetester' }], - isLoading: true, - isError: false, - fetchReporters, - }); + (useSuggestUserProfiles as jest.Mock).mockReturnValue({ data: userProfiles, isLoading: false }); }); it('should render the case status filter dropdown', () => { @@ -87,23 +84,20 @@ describe('CasesTableFilters ', () => { expect(onFilterChanged).toBeCalledWith({ tags: ['coke'] }); }); - it('should call onFilterChange when selected reporters change', () => { - const wrapper = mount( - - - - ); - wrapper - .find(`[data-test-subj="options-filter-popover-button-Reporter"]`) - .last() - .simulate('click'); + it('should call onFilterChange when selected assignees change', async () => { + const { getByTestId, getByText } = appMockRender.render(); + userEvent.click(getByTestId('options-filter-popover-button-assignees')); + await waitForEuiPopoverOpen(); - wrapper - .find(`[data-test-subj="options-filter-popover-item-casetester"]`) - .last() - .simulate('click'); + userEvent.click(getByText('Physical Dinosaur')); - expect(onFilterChanged).toBeCalledWith({ reporters: [{ username: 'casetester' }] }); + expect(onFilterChanged.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "assignees": Array [ + "u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0", + ], + } + `); }); it('should call onFilterChange when search changes', () => { @@ -157,23 +151,32 @@ describe('CasesTableFilters ', () => { expect(onFilterChanged).toHaveBeenCalledWith({ tags: ['pepsi'] }); }); - it('should remove reporter from selected reporters when reporter no longer exists', () => { - const ourProps = { + it('should remove assignee from selected assignees when assignee no longer exists', async () => { + const overrideProps = { ...props, initial: { ...DEFAULT_FILTER_OPTIONS, - reporters: [ - { username: 'casetester', full_name: null, email: null }, - { username: 'batman', full_name: null, email: null }, + assignees: [ + // invalid profile uid + '123', + 'u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0', ], }, }; - mount( - - - - ); - expect(onFilterChanged).toHaveBeenCalledWith({ reporters: [{ username: 'casetester' }] }); + + appMockRender.render(); + userEvent.click(screen.getByTestId('options-filter-popover-button-assignees')); + await waitForEuiPopoverOpen(); + + userEvent.click(screen.getByText('Physical Dinosaur')); + + expect(onFilterChanged.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "assignees": Array [ + "u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0", + ], + } + `); }); it('StatusFilterWrapper should have a fixed width of 180px', () => { @@ -269,6 +272,21 @@ describe('CasesTableFilters ', () => { expect(onFilterChanged).toBeCalledWith({ owner: [] }); }); + + it('does not select a solution on initial render', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find(`[data-test-subj="options-filter-popover-button-Solution"]`).first().props() + ).toEqual(expect.objectContaining({ hasActiveFilters: false })); + }); }); describe('create case button', () => { diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx index cedd7c9b647187..d35ff00058b999 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx @@ -10,10 +10,10 @@ import { isEqual } from 'lodash/fp'; import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiFilterGroup, EuiButton } from '@elastic/eui'; +import { UserProfileWithAvatar } from '@kbn/user-profile-components'; import { StatusAll, CaseStatusWithAllStatus, CaseSeverityWithAll } from '../../../common/ui/types'; import { CaseStatuses } from '../../../common/api'; import { FilterOptions } from '../../containers/types'; -import { useGetReporters } from '../../containers/use_get_reporters'; import { FilterPopover } from '../filter_popover'; import { StatusFilter } from './status_filter'; import * as i18n from './translations'; @@ -21,6 +21,8 @@ import { SeverityFilter } from './severity_filter'; import { useGetTags } from '../../containers/use_get_tags'; import { CASE_LIST_CACHE_KEY } from '../../containers/constants'; import { DEFAULT_FILTER_OPTIONS } from '../../containers/use_get_cases'; +import { AssigneesFilterPopover } from './assignees_filter'; +import { CurrentUserProfile } from '../types'; interface CasesTableFiltersProps { countClosedCases: number | null; @@ -33,6 +35,8 @@ interface CasesTableFiltersProps { availableSolutions: string[]; displayCreateCaseButton?: boolean; onCreateCasePressed?: () => void; + isLoading: boolean; + currentUserProfile: CurrentUserProfile; } // Fix the width of the status dropdown to prevent hiding long text items @@ -59,20 +63,18 @@ const CasesTableFiltersComponent = ({ availableSolutions, displayCreateCaseButton, onCreateCasePressed, + isLoading, + currentUserProfile, }: CasesTableFiltersProps) => { - const [selectedReporters, setSelectedReporters] = useState( - initial.reporters.map((r) => r.full_name ?? r.username ?? '') - ); const [search, setSearch] = useState(initial.search); const [selectedTags, setSelectedTags] = useState(initial.tags); - const [selectedOwner, setSelectedOwner] = useState(initial.owner); + const [selectedOwner, setSelectedOwner] = useState([]); + const [selectedAssignees, setSelectedAssignees] = useState([]); const { data: tags = [], refetch: fetchTags } = useGetTags(CASE_LIST_CACHE_KEY); - const { reporters, respReporters, fetchReporters } = useGetReporters(); const refetch = useCallback(() => { fetchTags(); - fetchReporters(); - }, [fetchReporters, fetchTags]); + }, [fetchTags]); useEffect(() => { if (setFilterRefetch != null) { @@ -80,26 +82,16 @@ const CasesTableFiltersComponent = ({ } }, [refetch, setFilterRefetch]); - const handleSelectedReporters = useCallback( - (newReporters) => { - if (!isEqual(newReporters, selectedReporters)) { - setSelectedReporters(newReporters); - const reportersObj = respReporters.filter( - (r) => newReporters.includes(r.username) || newReporters.includes(r.full_name) - ); - onFilterChanged({ reporters: reportersObj }); + const handleSelectedAssignees = useCallback( + (newAssignees: UserProfileWithAvatar[]) => { + if (!isEqual(newAssignees, selectedAssignees)) { + setSelectedAssignees(newAssignees); + onFilterChanged({ assignees: newAssignees.map((assignee) => assignee.uid) }); } }, - [selectedReporters, respReporters, onFilterChanged] + [selectedAssignees, onFilterChanged] ); - useEffect(() => { - if (selectedReporters.length) { - const newReporters = selectedReporters.filter((r) => reporters.includes(r)); - handleSelectedReporters(newReporters); - } - }, [handleSelectedReporters, reporters, selectedReporters]); - const handleSelectedTags = useCallback( (newTags) => { if (!isEqual(newTags, selectedTags)) { @@ -202,12 +194,11 @@ const CasesTableFiltersComponent = ({
- + i18n.translate('xpack.cases.allCasesView.totalFilteredUsers', { + defaultMessage: '{total, plural, one {# assignee} other {# assignees}} filtered', + values: { total }, + }); diff --git a/x-pack/plugins/cases/public/components/app/use_available_owners.ts b/x-pack/plugins/cases/public/components/app/use_available_owners.ts index 8715af5d6fa68c..c981a6a01e063f 100644 --- a/x-pack/plugins/cases/public/components/app/use_available_owners.ts +++ b/x-pack/plugins/cases/public/components/app/use_available_owners.ts @@ -8,8 +8,9 @@ import { APP_ID, FEATURE_ID } from '../../../common/constants'; import { useKibana } from '../../common/lib/kibana'; import { CasesPermissions } from '../../containers/types'; +import { allCasePermissions } from '../../utils/permissions'; -type Capability = Omit; +type Capability = Exclude; /** * @@ -18,17 +19,17 @@ type Capability = Omit; **/ export const useAvailableCasesOwners = ( - capabilities: Capability[] = ['create', 'read', 'update', 'delete', 'push'] + capabilities: Capability[] = allCasePermissions ): string[] => { const { capabilities: kibanaCapabilities } = useKibana().services.application; return Object.entries(kibanaCapabilities).reduce( - (availableOwners: string[], [featureId, kibananCapability]) => { + (availableOwners: string[], [featureId, kibanaCapability]) => { if (!featureId.endsWith('Cases')) { return availableOwners; } for (const cap of capabilities) { - const hasCapability = !!kibananCapability[`${cap}_cases`]; + const hasCapability = !!kibanaCapability[`${cap}_cases`]; if (!hasCapability) { return availableOwners; } diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx index 284785b33d7207..3855f14134f46b 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx @@ -21,6 +21,7 @@ import { useGetCaseUserActions } from '../../containers/use_get_case_user_action import { useGetTags } from '../../containers/use_get_tags'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; import { useUpdateCase } from '../../containers/use_update_case'; +import { useBulkGetUserProfiles } from '../../containers/user_profiles/use_bulk_get_user_profiles'; import { CaseViewPage } from './case_view_page'; import { caseData, @@ -40,6 +41,7 @@ jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/use_get_case'); jest.mock('../../containers/configure/use_connectors'); jest.mock('../../containers/use_post_push_to_service'); +jest.mock('../../containers/user_profiles/use_bulk_get_user_profiles'); jest.mock('../user_actions/timestamp', () => ({ UserActionTimestamp: () => <>, })); @@ -56,6 +58,7 @@ const useGetConnectorsMock = useGetConnectors as jest.Mock; const usePostPushToServiceMock = usePostPushToService as jest.Mock; const useGetCaseMetricsMock = useGetCaseMetrics as jest.Mock; const useGetTagsMock = useGetTags as jest.Mock; +const useBulkGetUserProfilesMock = useBulkGetUserProfiles as jest.Mock; const mockGetCase = (props: Partial = {}) => { const data = { @@ -96,6 +99,7 @@ describe('CaseViewPage', () => { usePostPushToServiceMock.mockReturnValue({ isLoading: false, pushCaseToExternalService }); useGetConnectorsMock.mockReturnValue({ data: connectorsMock, isLoading: false }); useGetTagsMock.mockReturnValue({ data: [], isLoading: false }); + useBulkGetUserProfilesMock.mockReturnValue({ data: new Map(), isLoading: false }); appMockRenderer = createAppMockRenderer(); }); diff --git a/x-pack/plugins/cases/public/components/case_view/components/assign_users.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/assign_users.test.tsx new file mode 100644 index 00000000000000..fdd4b5cb77e7d7 --- /dev/null +++ b/x-pack/plugins/cases/public/components/case_view/components/assign_users.test.tsx @@ -0,0 +1,402 @@ +/* + * 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 { useSuggestUserProfiles } from '../../../containers/user_profiles/use_suggest_user_profiles'; +import { useGetCurrentUserProfile } from '../../../containers/user_profiles/use_get_current_user_profile'; +import { userProfiles, userProfilesMap } from '../../../containers/user_profiles/api.mock'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; +import React from 'react'; +import { + AppMockRenderer, + createAppMockRenderer, + noUpdateCasesPermissions, +} from '../../../common/mock'; +import { AssignUsers, AssignUsersProps } from './assign_users'; +import { waitForEuiPopoverClose, waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; + +jest.mock('../../../containers/user_profiles/use_suggest_user_profiles'); +jest.mock('../../../containers/user_profiles/use_get_current_user_profile'); + +const useSuggestUserProfilesMock = useSuggestUserProfiles as jest.Mock; +const useGetCurrentUserProfileMock = useGetCurrentUserProfile as jest.Mock; + +const currentUserProfile = userProfiles[0]; + +describe('AssignUsers', () => { + let appMockRender: AppMockRenderer; + let defaultProps: AssignUsersProps; + + beforeEach(() => { + defaultProps = { + caseAssignees: [], + currentUserProfile, + userProfiles: new Map(), + onAssigneesChanged: jest.fn(), + isLoading: false, + }; + + useSuggestUserProfilesMock.mockReturnValue({ data: userProfiles, isLoading: false }); + useGetCurrentUserProfileMock.mockReturnValue({ data: currentUserProfile, isLoading: false }); + + appMockRender = createAppMockRenderer(); + }); + + it('does not show any assignees when there are none assigned', () => { + appMockRender.render(); + + expect(screen.getByText('No users have been assigned.')).toBeInTheDocument(); + }); + + it('does not show the suggest users edit button when the user does not have update permissions', () => { + appMockRender = createAppMockRenderer({ permissions: noUpdateCasesPermissions() }); + appMockRender.render(); + + expect(screen.queryByText('case-view-assignees-edit')).not.toBeInTheDocument(); + }); + + it('does not show the assign users link when the user does not have update permissions', () => { + appMockRender = createAppMockRenderer({ permissions: noUpdateCasesPermissions() }); + appMockRender.render(); + + expect(screen.queryByTestId('assign yourself')).not.toBeInTheDocument(); + expect(screen.queryByTestId('Assign a user')).not.toBeInTheDocument(); + }); + + it('does not show the suggest users edit button when the component is still loading', () => { + appMockRender.render(); + + expect(screen.queryByTestId('case-view-assignees-edit')).not.toBeInTheDocument(); + expect(screen.getByTestId('case-view-assignees-button-loading')).toBeInTheDocument(); + }); + + it('does not show the assign yourself link when the current profile is undefined', () => { + appMockRender.render(); + + expect(screen.queryByText('assign yourself')).not.toBeInTheDocument(); + expect(screen.getByText('Assign a user')).toBeInTheDocument(); + }); + + it('shows the suggest users edit button when the user has update permissions', () => { + appMockRender.render(); + + expect(screen.getByTestId('case-view-assignees-edit')).toBeInTheDocument(); + }); + + it('shows the two initially assigned users', () => { + const props = { + ...defaultProps, + caseAssignees: userProfiles.slice(0, 2), + userProfiles: userProfilesMap, + }; + appMockRender.render(); + + expect(screen.getByText('Damaged Raccoon')).toBeInTheDocument(); + expect(screen.getByText('Physical Dinosaur')).toBeInTheDocument(); + expect(screen.queryByText('Wet Dingo')).not.toBeInTheDocument(); + expect(screen.queryByText('No users have been assigned.')).not.toBeInTheDocument(); + expect(screen.queryByTestId('case-view-assignees-loading')).not.toBeInTheDocument(); + }); + + it('shows the rerendered assignees', () => { + const { rerender } = appMockRender.render(); + + const props = { + ...defaultProps, + caseAssignees: userProfiles.slice(0, 2), + userProfiles: userProfilesMap, + }; + rerender(); + + expect(screen.getByText('Damaged Raccoon')).toBeInTheDocument(); + expect(screen.getByText('Physical Dinosaur')).toBeInTheDocument(); + expect(screen.queryByText('Wet Dingo')).not.toBeInTheDocument(); + expect(screen.queryByText('No users have been assigned.')).not.toBeInTheDocument(); + expect(screen.queryByTestId('case-view-assignees-loading')).not.toBeInTheDocument(); + }); + + it('shows the popover when the pencil is clicked', async () => { + const props = { + ...defaultProps, + userProfiles: userProfilesMap, + }; + appMockRender.render(); + + fireEvent.click(screen.getByTestId('case-view-assignees-edit-button')); + await waitForEuiPopoverOpen(); + fireEvent.change(screen.getByPlaceholderText('Search users'), { + target: { value: 'damaged_raccoon@elastic.co' }, + }); + + expect(screen.getByText('Damaged Raccoon')).toBeInTheDocument(); + }); + + it('shows the popover when the assign a user link is clicked', async () => { + const props = { + ...defaultProps, + userProfiles: userProfilesMap, + }; + appMockRender.render(); + + fireEvent.click(screen.getByText('Assign a user')); + await waitForEuiPopoverOpen(); + fireEvent.change(screen.getByPlaceholderText('Search users'), { + target: { value: 'damaged_raccoon@elastic.co' }, + }); + + expect(screen.getByText('Damaged Raccoon')).toBeInTheDocument(); + }); + + it('assigns the current user when the assign yourself link is clicked', async () => { + const onAssigneesChanged = jest.fn(); + const props = { + ...defaultProps, + onAssigneesChanged, + userProfiles: userProfilesMap, + }; + appMockRender.render(); + + fireEvent.click(screen.getByText('assign yourself')); + + await waitFor(() => expect(onAssigneesChanged).toBeCalledTimes(1)); + + expect(onAssigneesChanged.mock.calls[0][0]).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object {}, + "enabled": true, + "uid": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0", + "user": Object { + "email": "damaged_raccoon@elastic.co", + "full_name": "Damaged Raccoon", + "username": "damaged_raccoon", + }, + }, + ] + `); + }); + + it('calls onAssigneesChanged with an empty array because all the users were deleted', async () => { + const onAssigneesChanged = jest.fn(); + const props = { + ...defaultProps, + caseAssignees: [{ uid: userProfiles[0].uid }], + onAssigneesChanged, + userProfiles: userProfilesMap, + }; + appMockRender.render(); + + fireEvent.mouseEnter( + screen.getByTestId(`user-profile-assigned-user-group-${userProfiles[0].user.username}`) + ); + fireEvent.click( + screen.getByTestId(`user-profile-assigned-user-cross-${userProfiles[0].user.username}`) + ); + + await waitFor(() => expect(onAssigneesChanged).toBeCalledTimes(1)); + + expect(onAssigneesChanged.mock.calls[0][0]).toMatchInlineSnapshot(`Array []`); + }); + + it('calls onAssigneesChanged when the popover is closed using the pencil button', async () => { + const onAssigneesChanged = jest.fn(); + const props = { + ...defaultProps, + onAssigneesChanged, + userProfiles: userProfilesMap, + }; + appMockRender.render(); + + fireEvent.click(screen.getByTestId('case-view-assignees-edit-button')); + await waitForEuiPopoverOpen(); + fireEvent.change(screen.getByPlaceholderText('Search users'), { + target: { value: 'damaged_raccoon@elastic.co' }, + }); + + fireEvent.click(screen.getByText('Damaged Raccoon')); + + // close the popover + fireEvent.click(screen.getByTestId('case-view-assignees-edit-button')); + await waitForEuiPopoverClose(); + + await waitFor(() => expect(onAssigneesChanged).toBeCalledTimes(1)); + + expect(onAssigneesChanged.mock.calls[0][0]).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object {}, + "enabled": true, + "uid": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0", + "user": Object { + "email": "damaged_raccoon@elastic.co", + "full_name": "Damaged Raccoon", + "username": "damaged_raccoon", + }, + }, + ] + `); + }); + + it('does not call onAssigneesChanged when the selected assignees have not changed between renders', async () => { + const onAssigneesChanged = jest.fn(); + const props = { + ...defaultProps, + caseAssignees: [{ uid: userProfiles[0].uid }], + onAssigneesChanged, + userProfiles: userProfilesMap, + }; + appMockRender.render(); + + fireEvent.click(screen.getByTestId('case-view-assignees-edit-button')); + await waitForEuiPopoverOpen(); + + // close the popover + fireEvent.click(screen.getByTestId('case-view-assignees-edit-button')); + await waitForEuiPopoverClose(); + + await waitFor(() => expect(onAssigneesChanged).toBeCalledTimes(0)); + }); + + it('calls onAssigneesChanged without unknownId1', async () => { + const onAssigneesChanged = jest.fn(); + const props = { + ...defaultProps, + caseAssignees: [{ uid: 'unknownId1' }, { uid: 'unknownId2' }], + onAssigneesChanged, + userProfiles: userProfilesMap, + }; + appMockRender.render(); + + fireEvent.mouseEnter(screen.getByTestId(`user-profile-assigned-user-group-unknownId1`)); + fireEvent.click(screen.getByTestId(`user-profile-assigned-user-cross-unknownId1`)); + + await waitFor(() => expect(onAssigneesChanged).toBeCalledTimes(1)); + + expect(onAssigneesChanged.mock.calls[0][0]).toMatchInlineSnapshot(` + Array [ + Object { + "uid": "unknownId2", + }, + ] + `); + }); + + it('renders two unknown users and one user with a profile', async () => { + const props = { + ...defaultProps, + caseAssignees: [{ uid: 'unknownId1' }, { uid: 'unknownId2' }, { uid: userProfiles[0].uid }], + userProfiles: userProfilesMap, + }; + appMockRender.render(); + + expect(screen.getByText('Damaged Raccoon')).toBeInTheDocument(); + expect(screen.getByTestId('user-profile-assigned-user-group-unknownId1')).toBeInTheDocument(); + expect(screen.getByTestId('user-profile-assigned-user-group-unknownId2')).toBeInTheDocument(); + }); + + it('calls onAssigneesChanged with both users with profiles and without', async () => { + const onAssigneesChanged = jest.fn(); + const props = { + ...defaultProps, + caseAssignees: [{ uid: 'unknownId1' }, { uid: 'unknownId2' }], + onAssigneesChanged, + userProfiles: userProfilesMap, + }; + appMockRender.render(); + + fireEvent.click(screen.getByTestId('case-view-assignees-edit-button')); + await waitForEuiPopoverOpen(); + fireEvent.change(screen.getByPlaceholderText('Search users'), { + target: { value: 'damaged_raccoon@elastic.co' }, + }); + + fireEvent.click(screen.getByText('Damaged Raccoon')); + + // close the popover + fireEvent.click(screen.getByTestId('case-view-assignees-edit-button')); + await waitForEuiPopoverClose(); + + await waitFor(() => expect(onAssigneesChanged).toBeCalledTimes(1)); + + expect(onAssigneesChanged.mock.calls[0][0]).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object {}, + "enabled": true, + "uid": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0", + "user": Object { + "email": "damaged_raccoon@elastic.co", + "full_name": "Damaged Raccoon", + "username": "damaged_raccoon", + }, + }, + Object { + "uid": "unknownId1", + }, + Object { + "uid": "unknownId2", + }, + ] + `); + }); + + it('calls onAssigneesChanged with the unknown users at the end', async () => { + const onAssigneesChanged = jest.fn(); + const props = { + ...defaultProps, + caseAssignees: [{ uid: userProfiles[1].uid }, { uid: 'unknownId1' }, { uid: 'unknownId2' }], + onAssigneesChanged, + userProfiles: userProfilesMap, + }; + appMockRender.render(); + + fireEvent.click(screen.getByTestId('case-view-assignees-edit-button')); + await waitForEuiPopoverOpen(); + + fireEvent.change(screen.getByPlaceholderText('Search users'), { + target: { value: 'damaged_raccoon@elastic.co' }, + }); + + fireEvent.click(screen.getByText('Damaged Raccoon')); + + // close the popover + fireEvent.click(screen.getByTestId('case-view-assignees-edit-button')); + await waitForEuiPopoverClose(); + + await waitFor(() => expect(onAssigneesChanged).toBeCalledTimes(1)); + + expect(onAssigneesChanged.mock.calls[0][0]).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object {}, + "enabled": true, + "uid": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0", + "user": Object { + "email": "damaged_raccoon@elastic.co", + "full_name": "Damaged Raccoon", + "username": "damaged_raccoon", + }, + }, + Object { + "data": Object {}, + "enabled": true, + "uid": "u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0", + "user": Object { + "email": "physical_dinosaur@elastic.co", + "full_name": "Physical Dinosaur", + "username": "physical_dinosaur", + }, + }, + Object { + "uid": "unknownId1", + }, + Object { + "uid": "unknownId2", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/cases/public/components/case_view/components/assign_users.tsx b/x-pack/plugins/cases/public/components/case_view/components/assign_users.tsx new file mode 100644 index 00000000000000..520c76fef51243 --- /dev/null +++ b/x-pack/plugins/cases/public/components/case_view/components/assign_users.tsx @@ -0,0 +1,213 @@ +/* + * 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, { useCallback, useEffect, useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiLink, + EuiLoadingSpinner, + EuiSpacer, +} from '@elastic/eui'; + +import { UserProfileWithAvatar } from '@kbn/user-profile-components'; +import { CasesPermissions } from '../../../../common'; +import { useAssignees } from '../../../containers/user_profiles/use_assignees'; +import { CaseAssignees } from '../../../../common/api/cases/assignee'; +import * as i18n from '../translations'; +import { SidebarTitle } from './sidebar_title'; +import { UserRepresentation } from '../../user_profiles/user_representation'; +import { useCasesContext } from '../../cases_context/use_cases_context'; +import { Assignee } from '../../user_profiles/types'; +import { SuggestUsersPopover } from './suggest_users_popover'; +import { CurrentUserProfile } from '../../types'; + +interface AssigneesListProps { + assignees: Assignee[]; + currentUserProfile: CurrentUserProfile; + permissions: CasesPermissions; + assignSelf: () => void; + togglePopOver: () => void; + onAssigneeRemoved: (removedAssigneeUID: string) => void; +} + +const AssigneesList: React.FC = ({ + assignees, + currentUserProfile, + permissions, + assignSelf, + togglePopOver, + onAssigneeRemoved, +}) => { + return ( + <> + {assignees.length === 0 ? ( + + + +

+ {i18n.NO_ASSIGNEES} + {permissions.update && ( + <> +
+ + {i18n.ASSIGN_A_USER} + + + )} + {currentUserProfile && permissions.update && ( + <> + {i18n.SPACED_OR} + + {i18n.ASSIGN_YOURSELF} + + + )} +

+
+
+
+ ) : ( + + {assignees.map((assignee) => ( + + + + ))} + + )} + + ); +}; +AssigneesList.displayName = 'AssigneesList'; + +export interface AssignUsersProps { + caseAssignees: CaseAssignees; + currentUserProfile: CurrentUserProfile; + userProfiles: Map; + onAssigneesChanged: (assignees: Assignee[]) => void; + isLoading: boolean; +} + +const AssignUsersComponent: React.FC = ({ + caseAssignees, + userProfiles, + currentUserProfile, + onAssigneesChanged, + isLoading, +}) => { + const { assigneesWithProfiles, assigneesWithoutProfiles, allAssignees } = useAssignees({ + caseAssignees, + userProfiles, + currentUserProfile, + }); + + const [selectedAssignees, setSelectedAssignees] = useState(); + const [needToUpdateAssignees, setNeedToUpdateAssignees] = useState(false); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const togglePopover = useCallback(() => { + setIsPopoverOpen((value) => !value); + setNeedToUpdateAssignees(true); + }, []); + + const onClosePopover = useCallback(() => { + // Order matters here because needToUpdateAssignees will likely be true already + // from the togglePopover call when opening the popover, so if we set the popover to false + // first, we'll get a rerender and then get another after we set needToUpdateAssignees to true again + setNeedToUpdateAssignees(true); + setIsPopoverOpen(false); + }, []); + + const onAssigneeRemoved = useCallback( + (removedAssigneeUID: string) => { + const remainingAssignees = allAssignees.filter( + (assignee) => assignee.uid !== removedAssigneeUID + ); + setSelectedAssignees(remainingAssignees); + setNeedToUpdateAssignees(true); + }, + [allAssignees] + ); + + const onUsersChange = useCallback( + (users: UserProfileWithAvatar[]) => { + // if users are selected then also include the users without profiles + if (users.length > 0) { + setSelectedAssignees([...users, ...assigneesWithoutProfiles]); + } else { + // all users were deselected so lets remove the users without profiles as well + setSelectedAssignees([]); + } + }, + [assigneesWithoutProfiles] + ); + + const assignSelf = useCallback(() => { + if (!currentUserProfile) { + return; + } + + const newAssignees = [currentUserProfile, ...allAssignees]; + setSelectedAssignees(newAssignees); + setNeedToUpdateAssignees(true); + }, [currentUserProfile, allAssignees]); + + const { permissions } = useCasesContext(); + + useEffect(() => { + // selectedAssignees will be undefined on initial render or a rerender occurs, so we only want to update the assignees + // after the users have been changed in some manner not when it is an initial value + if (isPopoverOpen === false && needToUpdateAssignees && selectedAssignees) { + setNeedToUpdateAssignees(false); + onAssigneesChanged(selectedAssignees); + } + }, [isPopoverOpen, needToUpdateAssignees, onAssigneesChanged, selectedAssignees]); + + return ( + + + + + + {isLoading && } + {!isLoading && permissions.update && ( + + + + )} + + + + + ); +}; + +AssignUsersComponent.displayName = 'AssignUsers'; + +export const AssignUsers = React.memo(AssignUsersComponent); diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.test.tsx index db5411525290b5..74ff651a0a11c5 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.test.tsx @@ -26,6 +26,7 @@ import { useGetCaseUserActions } from '../../../containers/use_get_case_user_act import { usePostPushToService } from '../../../containers/use_post_push_to_service'; import { useGetConnectors } from '../../../containers/configure/use_connectors'; import { useGetTags } from '../../../containers/use_get_tags'; +import { useBulkGetUserProfiles } from '../../../containers/user_profiles/use_bulk_get_user_profiles'; jest.mock('../../../containers/use_get_case_user_actions'); jest.mock('../../../containers/configure/use_connectors'); @@ -36,6 +37,7 @@ jest.mock('../../user_actions/timestamp', () => ({ jest.mock('../../../common/navigation/hooks'); jest.mock('../../../containers/use_get_action_license'); jest.mock('../../../containers/use_get_tags'); +jest.mock('../../../containers/user_profiles/use_bulk_get_user_profiles'); (useGetTags as jest.Mock).mockReturnValue({ data: ['coke', 'pepsi'], refetch: jest.fn() }); @@ -93,12 +95,14 @@ export const caseProps = { const useGetCaseUserActionsMock = useGetCaseUserActions as jest.Mock; const useGetConnectorsMock = useGetConnectors as jest.Mock; const usePostPushToServiceMock = usePostPushToService as jest.Mock; +const useBulkGetUserProfilesMock = useBulkGetUserProfiles as jest.Mock; describe('Case View Page activity tab', () => { beforeAll(() => { useGetCaseUserActionsMock.mockReturnValue(defaultUseGetCaseUserActions); useGetConnectorsMock.mockReturnValue({ data: connectorsMock, isLoading: false }); usePostPushToServiceMock.mockReturnValue({ isLoading: false, pushCaseToExternalService }); + useBulkGetUserProfilesMock.mockReturnValue({ isLoading: false, data: new Map() }); }); let appMockRender: AppMockRenderer; diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx index 1e935c15891f42..647763d5461d19 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx @@ -7,6 +7,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingContent } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; +import { isEqual, uniq } from 'lodash'; +import { useGetCurrentUserProfile } from '../../../containers/user_profiles/use_get_current_user_profile'; +import { useBulkGetUserProfiles } from '../../../containers/user_profiles/use_bulk_get_user_profiles'; import { useGetConnectors } from '../../../containers/configure/use_connectors'; import { CaseSeverity } from '../../../../common/api'; import { useCaseViewNavigation } from '../../../common/navigation'; @@ -15,9 +18,9 @@ import { Case, CaseStatuses } from '../../../../common'; import { EditConnector } from '../../edit_connector'; import { CasesNavigation } from '../../links'; import { StatusActionButton } from '../../status/button'; -import { TagList } from '../../tag_list'; +import { EditTags } from './edit_tags'; import { UserActions } from '../../user_actions'; -import { UserList } from '../../user_list'; +import { UserList } from './user_list'; import { useOnUpdateField } from '../use_on_update_field'; import { useCasesContext } from '../../cases_context/use_cases_context'; import * as i18n from '../translations'; @@ -25,6 +28,9 @@ import { getNoneConnector, normalizeActionConnector } from '../../configure_case import { getConnectorById } from '../../utils'; import { SeveritySidebarSelector } from '../../severity/sidebar_selector'; import { useGetCaseUserActions } from '../../../containers/use_get_case_user_actions'; +import { AssignUsers } from './assign_users'; +import { SidebarSection } from './sidebar_section'; +import { Assignee } from '../../user_profiles/types'; export const CaseViewActivity = ({ ruleDetailsNavigation, @@ -47,6 +53,21 @@ export const CaseViewActivity = ({ caseData.connector.id ); + const assignees = useMemo( + () => caseData.assignees.map((assignee) => assignee.uid), + [caseData.assignees] + ); + + const userActionProfileUids = Array.from(userActionsData?.profileUids?.values() ?? []); + const uidsToRetrieve = uniq([...userActionProfileUids, ...assignees]); + + const { data: userProfiles, isFetching: isLoadingUserProfiles } = useBulkGetUserProfiles({ + uids: uidsToRetrieve, + }); + + const { data: currentUserProfile, isFetching: isLoadingCurrentUserProfile } = + useGetCurrentUserProfile(); + const onShowAlertDetails = useCallback( (alertId: string, index: string) => { if (showAlertDetails) { @@ -61,6 +82,11 @@ export const CaseViewActivity = ({ caseData, }); + const isLoadingAssigneeData = + (isLoading && loadingKey === 'assignees') || + isLoadingUserProfiles || + isLoadingCurrentUserProfile; + const changeStatus = useCallback( (status: CaseStatuses) => onUpdateField({ @@ -88,6 +114,16 @@ export const CaseViewActivity = ({ [onUpdateField] ); + const onUpdateAssignees = useCallback( + (newAssignees: Assignee[]) => { + const newAssigneeUids = newAssignees.map((assignee) => ({ uid: assignee.uid })); + if (!isEqual(newAssigneeUids.sort(), assignees.sort())) { + onUpdateField({ key: 'assignees', value: newAssigneeUids }); + } + }, + [assignees, onUpdateField] + ); + const { isLoading: isLoadingConnectors, data: connectors = [] } = useGetConnectors(); const [connectorName, isValidConnector] = useMemo(() => { @@ -118,10 +154,12 @@ export const CaseViewActivity = ({ {isLoadingUserActions && ( )} - {!isLoadingUserActions && userActionsData && ( + {!isLoadingUserActions && userActionsData && userProfiles && ( + + + ) : null} - ({ @@ -32,13 +32,13 @@ jest.mock('@elastic/eui', () => { }; }); const onSubmit = jest.fn(); -const defaultProps: TagListProps = { +const defaultProps: EditTagsProps = { isLoading: false, onSubmit, tags: [], }; -describe('TagList ', () => { +describe('EditTags ', () => { const sampleTags = ['coke', 'pepsi']; const fetchTags = jest.fn(); const formHookMock = getFormMock({ tags: sampleTags }); @@ -55,7 +55,7 @@ describe('TagList ', () => { it('Renders no tags, and then edit', () => { const wrapper = mount( - + ); expect(wrapper.find(`[data-test-subj="no-tags"]`).last().exists()).toBeTruthy(); @@ -67,7 +67,7 @@ describe('TagList ', () => { it('Edit tag on submit', async () => { const wrapper = mount( - + ); wrapper.find(`[data-test-subj="tag-list-edit-button"]`).last().simulate('click'); @@ -78,7 +78,7 @@ describe('TagList ', () => { it('Tag options render with new tags added', () => { const wrapper = mount( - + ); wrapper.find(`[data-test-subj="tag-list-edit-button"]`).last().simulate('click'); @@ -94,7 +94,7 @@ describe('TagList ', () => { }; const wrapper = mount( - + ); @@ -110,7 +110,7 @@ describe('TagList ', () => { it('does not render when the user does not have update permissions', () => { const wrapper = mount( - + ); expect(wrapper.find(`[data-test-subj="tag-list-edit"]`).exists()).toBeFalsy(); diff --git a/x-pack/plugins/cases/public/components/tag_list/index.tsx b/x-pack/plugins/cases/public/components/case_view/components/edit_tags.tsx similarity index 90% rename from x-pack/plugins/cases/public/components/tag_list/index.tsx rename to x-pack/plugins/cases/public/components/case_view/components/edit_tags.tsx index c85f989f882819..0dd651f6c92515 100644 --- a/x-pack/plugins/cases/public/components/tag_list/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/edit_tags.tsx @@ -18,17 +18,27 @@ import { } from '@elastic/eui'; import styled, { css } from 'styled-components'; import { isEqual } from 'lodash/fp'; -import * as i18n from './translations'; -import { Form, FormDataProvider, useForm, getUseField, Field } from '../../common/shared_imports'; -import { schema } from './schema'; -import { useGetTags } from '../../containers/use_get_tags'; +import * as i18n from '../../tags/translations'; +import { + Form, + FormDataProvider, + useForm, + getUseField, + Field, + FormSchema, +} from '../../../common/shared_imports'; +import { useGetTags } from '../../../containers/use_get_tags'; +import { Tags } from '../../tags/tags'; +import { useCasesContext } from '../../cases_context/use_cases_context'; +import { schemaTags } from '../../create/schema'; -import { Tags } from './tags'; -import { useCasesContext } from '../cases_context/use_cases_context'; +export const schema: FormSchema = { + tags: schemaTags, +}; const CommonUseField = getUseField({ component: Field }); -export interface TagListProps { +export interface EditTagsProps { isLoading: boolean; onSubmit: (a: string[]) => void; tags: string[]; @@ -55,7 +65,7 @@ const ColumnFlexGroup = styled(EuiFlexGroup)` `} `; -export const TagList = React.memo(({ isLoading, onSubmit, tags }: TagListProps) => { +export const EditTags = React.memo(({ isLoading, onSubmit, tags }: EditTagsProps) => { const { permissions } = useCasesContext(); const initialState = { tags }; const { form } = useForm({ @@ -194,4 +204,4 @@ export const TagList = React.memo(({ isLoading, onSubmit, tags }: TagListProps) ); }); -TagList.displayName = 'TagList'; +EditTags.displayName = 'EditTags'; diff --git a/x-pack/plugins/cases/public/components/case_view/components/sidebar_section.tsx b/x-pack/plugins/cases/public/components/case_view/components/sidebar_section.tsx new file mode 100644 index 00000000000000..d7b61e16b36b69 --- /dev/null +++ b/x-pack/plugins/cases/public/components/case_view/components/sidebar_section.tsx @@ -0,0 +1,30 @@ +/* + * 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 from 'react'; +import { EuiHorizontalRule } from '@elastic/eui'; + +interface SidebarSectionProps { + children: React.ReactNode; + showHorizontalRule?: boolean; +} + +const SidebarSectionComponent: React.FC = ({ + children, + showHorizontalRule = true, +}) => { + return ( + <> + {children} + {showHorizontalRule ? : null} + + ); +}; + +SidebarSectionComponent.displayName = 'SidebarSection'; + +export const SidebarSection = React.memo(SidebarSectionComponent); diff --git a/x-pack/plugins/cases/public/components/case_view/components/sidebar_title.tsx b/x-pack/plugins/cases/public/components/case_view/components/sidebar_title.tsx new file mode 100644 index 00000000000000..b9ee1dd871b8e0 --- /dev/null +++ b/x-pack/plugins/cases/public/components/case_view/components/sidebar_title.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiTitle } from '@elastic/eui'; + +interface SidebarTitleProps { + title: string; +} + +const SidebarTitleComponent: React.FC = ({ title }) => { + return ( + +

{title}

+
+ ); +}; + +SidebarTitleComponent.displayName = 'SidebarTitle'; + +export const SidebarTitle = React.memo(SidebarTitleComponent); diff --git a/x-pack/plugins/cases/public/components/case_view/components/suggest_users_popover.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/suggest_users_popover.test.tsx new file mode 100644 index 00000000000000..27115acf5697f0 --- /dev/null +++ b/x-pack/plugins/cases/public/components/case_view/components/suggest_users_popover.test.tsx @@ -0,0 +1,238 @@ +/* + * 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 from 'react'; +import { AppMockRenderer, createAppMockRenderer } from '../../../common/mock'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; +import { SuggestUsersPopoverProps, SuggestUsersPopover } from './suggest_users_popover'; +import { userProfiles } from '../../../containers/user_profiles/api.mock'; +import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; +import { UserProfileWithAvatar } from '@kbn/user-profile-components'; +import { AssigneeWithProfile } from '../../user_profiles/types'; + +jest.mock('../../../containers/user_profiles/api'); + +describe('SuggestUsersPopover', () => { + let appMockRender: AppMockRenderer; + let defaultProps: SuggestUsersPopoverProps; + + beforeEach(() => { + appMockRender = createAppMockRenderer(); + + defaultProps = { + isLoading: false, + assignedUsersWithProfiles: [], + isPopoverOpen: true, + onUsersChange: jest.fn(), + togglePopover: jest.fn(), + onClosePopover: jest.fn(), + currentUserProfile: undefined, + }; + }); + + it('calls onUsersChange when 1 user is selected', async () => { + const onUsersChange = jest.fn(); + const props = { ...defaultProps, onUsersChange }; + appMockRender.render(); + + await waitForEuiPopoverOpen(); + + fireEvent.change(screen.getByPlaceholderText('Search users'), { target: { value: 'dingo' } }); + + await waitFor(() => { + expect(screen.getByText('wet_dingo@elastic.co')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('wet_dingo@elastic.co')); + + expect(onUsersChange.mock.calls[0][0]).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object {}, + "enabled": true, + "uid": "u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0", + "user": Object { + "email": "wet_dingo@elastic.co", + "full_name": "Wet Dingo", + "username": "wet_dingo", + }, + }, + ] + `); + }); + + it('calls onUsersChange when multiple users are selected', async () => { + const onUsersChange = jest.fn(); + const props = { ...defaultProps, onUsersChange }; + appMockRender.render(); + + await waitForEuiPopoverOpen(); + + fireEvent.change(screen.getByPlaceholderText('Search users'), { target: { value: 'elastic' } }); + + await waitFor(() => { + expect(screen.getByText('wet_dingo@elastic.co')).toBeInTheDocument(); + expect(screen.getByText('damaged_raccoon@elastic.co')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('wet_dingo@elastic.co')); + fireEvent.click(screen.getByText('damaged_raccoon@elastic.co')); + + expect(onUsersChange.mock.calls[1][0]).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object {}, + "enabled": true, + "uid": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0", + "user": Object { + "email": "damaged_raccoon@elastic.co", + "full_name": "Damaged Raccoon", + "username": "damaged_raccoon", + }, + }, + Object { + "data": Object {}, + "enabled": true, + "uid": "u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0", + "user": Object { + "email": "wet_dingo@elastic.co", + "full_name": "Wet Dingo", + "username": "wet_dingo", + }, + }, + ] + `); + }); + + it('calls onUsersChange with the current user (Physical Dinosaur) at the beginning', async () => { + const onUsersChange = jest.fn(); + const props = { + ...defaultProps, + assignedUsersWithProfiles: [asAssignee(userProfiles[1]), asAssignee(userProfiles[0])], + currentUserProfile: userProfiles[1], + onUsersChange, + }; + appMockRender.render(); + + await waitForEuiPopoverOpen(); + + fireEvent.change(screen.getByPlaceholderText('Search users'), { target: { value: 'elastic' } }); + + await waitFor(() => { + expect(screen.getByText('wet_dingo@elastic.co')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('wet_dingo@elastic.co')); + + expect(onUsersChange.mock.calls[0][0]).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object {}, + "enabled": true, + "uid": "u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0", + "user": Object { + "email": "physical_dinosaur@elastic.co", + "full_name": "Physical Dinosaur", + "username": "physical_dinosaur", + }, + }, + Object { + "data": Object {}, + "enabled": true, + "uid": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0", + "user": Object { + "email": "damaged_raccoon@elastic.co", + "full_name": "Damaged Raccoon", + "username": "damaged_raccoon", + }, + }, + Object { + "data": Object {}, + "enabled": true, + "uid": "u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0", + "user": Object { + "email": "wet_dingo@elastic.co", + "full_name": "Wet Dingo", + "username": "wet_dingo", + }, + }, + ] + `); + }); + + it('does not show the assigned users total if there are no assigned users', async () => { + appMockRender.render(); + + await waitForEuiPopoverOpen(); + + expect(screen.queryByText('assigned')).not.toBeInTheDocument(); + fireEvent.change(screen.getByPlaceholderText('Search users'), { target: { value: 'dingo' } }); + + await waitFor(() => { + expect(screen.getByText('wet_dingo@elastic.co')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('wet_dingo@elastic.co')); + expect(screen.getByText('1 assigned')).toBeInTheDocument(); + }); + + it('shows the 1 assigned total after clicking on a user', async () => { + appMockRender.render(); + + await waitForEuiPopoverOpen(); + + expect(screen.queryByText('assigned')).not.toBeInTheDocument(); + fireEvent.change(screen.getByPlaceholderText('Search users'), { target: { value: 'dingo' } }); + fireEvent.click(screen.getByText('wet_dingo@elastic.co')); + expect(screen.getByText('1 assigned')).toBeInTheDocument(); + }); + + it('shows the 1 assigned total when the users are passed in', async () => { + const props = { + ...defaultProps, + assignedUsersWithProfiles: [{ uid: userProfiles[0].uid, profile: userProfiles[0] }], + }; + appMockRender.render(); + + await waitForEuiPopoverOpen(); + + expect(screen.getByText('1 assigned')).toBeInTheDocument(); + expect(screen.getByText('Damaged Raccoon')).toBeInTheDocument(); + }); + + it('calls onTogglePopover when clicking the edit button after the popover is already open', async () => { + const togglePopover = jest.fn(); + const props = { + ...defaultProps, + togglePopover, + }; + appMockRender.render(); + + await waitForEuiPopoverOpen(); + + await waitFor(() => { + expect(screen.getByTestId('case-view-assignees-edit-button')).not.toBeDisabled(); + }); + + fireEvent.click(screen.getByTestId('case-view-assignees-edit-button')); + + expect(togglePopover).toBeCalled(); + }); + + it('shows results initially', async () => { + appMockRender.render(); + + await waitForEuiPopoverOpen(); + + await waitFor(() => expect(screen.getByText('Damaged Raccoon')).toBeInTheDocument()); + }); +}); + +const asAssignee = (profile: UserProfileWithAvatar): AssigneeWithProfile => ({ + uid: profile.uid, + profile, +}); diff --git a/x-pack/plugins/cases/public/components/case_view/components/suggest_users_popover.tsx b/x-pack/plugins/cases/public/components/case_view/components/suggest_users_popover.tsx new file mode 100644 index 00000000000000..cb824adf0a2178 --- /dev/null +++ b/x-pack/plugins/cases/public/components/case_view/components/suggest_users_popover.tsx @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import { UserProfilesPopover, UserProfileWithAvatar } from '@kbn/user-profile-components'; + +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { isEmpty } from 'lodash'; +import { useSuggestUserProfiles } from '../../../containers/user_profiles/use_suggest_user_profiles'; +import { useCasesContext } from '../../cases_context/use_cases_context'; +import { AssigneeWithProfile } from '../../user_profiles/types'; +import * as i18n from '../translations'; +import { bringCurrentUserToFrontAndSort } from '../../user_profiles/sort'; +import { SelectedStatusMessage } from '../../user_profiles/selected_status_message'; +import { EmptyMessage } from '../../user_profiles/empty_message'; +import { NoMatches } from '../../user_profiles/no_matches'; +import { CurrentUserProfile } from '../../types'; + +const PopoverButton: React.FC<{ togglePopover: () => void; isDisabled: boolean }> = ({ + togglePopover, + isDisabled, +}) => ( + + + +); +PopoverButton.displayName = 'PopoverButton'; + +export interface SuggestUsersPopoverProps { + assignedUsersWithProfiles: AssigneeWithProfile[]; + currentUserProfile: CurrentUserProfile; + isLoading: boolean; + isPopoverOpen: boolean; + onUsersChange: (users: UserProfileWithAvatar[]) => void; + togglePopover: () => void; + onClosePopover: () => void; +} + +const SuggestUsersPopoverComponent: React.FC = ({ + assignedUsersWithProfiles, + currentUserProfile, + isLoading, + isPopoverOpen, + onUsersChange, + togglePopover, + onClosePopover, +}) => { + const { owner } = useCasesContext(); + const [searchTerm, setSearchTerm] = useState(''); + + const selectedProfiles = useMemo(() => { + return bringCurrentUserToFrontAndSort( + currentUserProfile, + assignedUsersWithProfiles.map((assignee) => ({ ...assignee.profile })) + ); + }, [assignedUsersWithProfiles, currentUserProfile]); + + const [selectedUsers, setSelectedUsers] = useState(); + const [isUserTyping, setIsUserTyping] = useState(false); + + const onChange = useCallback( + (users: UserProfileWithAvatar[]) => { + const sortedUsers = bringCurrentUserToFrontAndSort(currentUserProfile, users); + setSelectedUsers(sortedUsers); + onUsersChange(sortedUsers ?? []); + }, + [currentUserProfile, onUsersChange] + ); + + const selectedStatusMessage = useCallback( + (selectedCount: number) => ( + + ), + [] + ); + + const onDebounce = useCallback(() => setIsUserTyping(false), []); + + const { + data: userProfiles, + isLoading: isLoadingSuggest, + isFetching: isFetchingSuggest, + } = useSuggestUserProfiles({ + name: searchTerm, + owners: owner, + onDebounce, + }); + + const isLoadingData = isLoadingSuggest || isLoading || isFetchingSuggest || isUserTyping; + const isDisabled = isLoading; + + const searchResultProfiles = useMemo( + () => bringCurrentUserToFrontAndSort(currentUserProfile, userProfiles), + [currentUserProfile, userProfiles] + ); + + return ( + } + isOpen={isPopoverOpen} + closePopover={onClosePopover} + panelStyle={{ + minWidth: 520, + }} + selectableProps={{ + onChange, + onSearchChange: (term) => { + setSearchTerm(term); + + if (!isEmpty(term)) { + setIsUserTyping(true); + } + }, + selectedStatusMessage, + options: searchResultProfiles, + selectedOptions: selectedUsers ?? selectedProfiles, + isLoading: isLoadingData, + height: 'full', + searchPlaceholder: i18n.SEARCH_USERS, + clearButtonLabel: i18n.REMOVE_ASSIGNEES, + emptyMessage: , + noMatchesMessage: !isLoadingData ? : , + }} + /> + ); +}; + +SuggestUsersPopoverComponent.displayName = 'SuggestUsersPopover'; + +export const SuggestUsersPopover = React.memo(SuggestUsersPopoverComponent); diff --git a/x-pack/plugins/cases/public/components/user_list/index.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/user_list.test.tsx similarity index 93% rename from x-pack/plugins/cases/public/components/user_list/index.test.tsx rename to x-pack/plugins/cases/public/components/case_view/components/user_list.test.tsx index 70f9e7d2fbdfc6..ab3b0f90c65a6d 100644 --- a/x-pack/plugins/cases/public/components/user_list/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/user_list.test.tsx @@ -7,18 +7,20 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { UserList } from '.'; -import * as i18n from '../case_view/translations'; +import { UserList } from './user_list'; +import * as i18n from '../translations'; describe('UserList ', () => { const title = 'Case Title'; const caseLink = 'http://reddit.com'; const user = { username: 'username', fullName: 'Full Name', email: 'testemail@elastic.co' }; const open = jest.fn(); + beforeEach(() => { jest.clearAllMocks(); window.open = open; }); + it('triggers mailto when email icon clicked', () => { const wrapper = shallow( + i18n.translate('xpack.cases.caseView.sendEmalLinkAria', { + values: { user }, + defaultMessage: 'click to send an email to {user}', + }); + +export const EDIT_ASSIGNEES_ARIA_LABEL = i18n.translate( + 'xpack.cases.caseView.editAssigneesAriaLabel', + { + defaultMessage: 'click to edit assignees', + } +); + +export const NO_ASSIGNEES = i18n.translate('xpack.cases.caseView.noAssignees', { + defaultMessage: 'No users have been assigned.', +}); + +export const ASSIGN_A_USER = i18n.translate('xpack.cases.caseView.assignUser', { + defaultMessage: 'Assign a user', +}); + +export const SPACED_OR = i18n.translate('xpack.cases.caseView.spacedOrText', { + defaultMessage: ' or ', +}); + +export const ASSIGN_YOURSELF = i18n.translate('xpack.cases.caseView.assignYourself', { + defaultMessage: 'assign yourself', +}); + +export const TOTAL_USERS_ASSIGNED = (total: number) => + i18n.translate('xpack.cases.caseView.totalUsersAssigned', { + defaultMessage: '{total} assigned', + values: { total }, + }); diff --git a/x-pack/plugins/cases/public/components/case_view/use_on_update_field.ts b/x-pack/plugins/cases/public/components/case_view/use_on_update_field.ts index 33620c91d87a2d..9180244b9cf456 100644 --- a/x-pack/plugins/cases/public/components/case_view/use_on_update_field.ts +++ b/x-pack/plugins/cases/public/components/case_view/use_on_update_field.ts @@ -6,6 +6,8 @@ */ import { useCallback } from 'react'; +import deepEqual from 'fast-deep-equal'; + import { CaseConnector } from '../../../common/api'; import { CaseAttributes } from '../../../common/api/cases/case'; import { CaseStatuses } from '../../../common/api/cases/status'; @@ -68,6 +70,13 @@ export const useOnUpdateField = ({ caseData, caseId }: { caseData: Case; caseId: if (caseData.severity !== value) { callUpdate('severity', severityUpdate); } + break; + case 'assignees': + const assigneesUpdate = getTypedPayload(value); + if (!deepEqual(caseData.assignees, value)) { + callUpdate('assignees', assigneesUpdate); + } + break; default: return null; } diff --git a/x-pack/plugins/cases/public/components/create/assignees.test.tsx b/x-pack/plugins/cases/public/components/create/assignees.test.tsx new file mode 100644 index 00000000000000..2ff593651abf79 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/assignees.test.tsx @@ -0,0 +1,153 @@ +/* + * 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 from 'react'; +import userEvent from '@testing-library/user-event'; +import { AppMockRenderer, createAppMockRenderer } from '../../common/mock'; + +import { useForm, Form, FormHook } from '../../common/shared_imports'; +import { userProfiles } from '../../containers/user_profiles/api.mock'; +import { Assignees } from './assignees'; +import { FormProps } from './schema'; +import { act, waitFor } from '@testing-library/react'; +import * as api from '../../containers/user_profiles/api'; +import { UserProfile } from '@kbn/user-profile-components'; + +jest.mock('../../containers/user_profiles/api'); + +const currentUserProfile = userProfiles[0]; + +describe('Assignees', () => { + let globalForm: FormHook; + let appMockRender: AppMockRenderer; + + const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm(); + globalForm = form; + + return
{children}; + }; + + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('renders', async () => { + const result = appMockRender.render( + + + + ); + + await waitFor(() => { + expect(result.getByTestId('comboBoxSearchInput')).not.toBeDisabled(); + }); + + expect(result.getByTestId('createCaseAssigneesComboBox')).toBeInTheDocument(); + }); + + it('does not render the assign yourself link when the current user profile is undefined', async () => { + const spyOnGetCurrentUserProfile = jest.spyOn(api, 'getCurrentUserProfile'); + spyOnGetCurrentUserProfile.mockResolvedValue(undefined as unknown as UserProfile); + + const result = appMockRender.render( + + + + ); + + await waitFor(() => { + expect(result.getByTestId('comboBoxSearchInput')).not.toBeDisabled(); + }); + + expect(result.queryByTestId('create-case-assign-yourself-link')).not.toBeInTheDocument(); + expect(result.getByTestId('createCaseAssigneesComboBox')).toBeInTheDocument(); + }); + + it('selects the current user correctly', async () => { + const spyOnGetCurrentUserProfile = jest.spyOn(api, 'getCurrentUserProfile'); + spyOnGetCurrentUserProfile.mockResolvedValue(currentUserProfile); + + const result = appMockRender.render( + + + + ); + + await waitFor(() => { + expect(result.getByTestId('comboBoxSearchInput')).not.toBeDisabled(); + }); + + act(() => { + userEvent.click(result.getByTestId('create-case-assign-yourself-link')); + }); + + await waitFor(() => { + expect(globalForm.getFormData()).toEqual({ assignees: [{ uid: currentUserProfile.uid }] }); + }); + }); + + it('disables the assign yourself button if the current user is already selected', async () => { + const spyOnGetCurrentUserProfile = jest.spyOn(api, 'getCurrentUserProfile'); + spyOnGetCurrentUserProfile.mockResolvedValue(currentUserProfile); + + const result = appMockRender.render( + + + + ); + + await waitFor(() => { + expect(result.getByTestId('comboBoxSearchInput')).not.toBeDisabled(); + }); + + act(() => { + userEvent.click(result.getByTestId('create-case-assign-yourself-link')); + }); + + await waitFor(() => { + expect(globalForm.getFormData()).toEqual({ assignees: [{ uid: currentUserProfile.uid }] }); + }); + + expect(result.getByTestId('create-case-assign-yourself-link')).toBeDisabled(); + }); + + it('assignees users correctly', async () => { + const result = appMockRender.render( + + + + ); + + await waitFor(() => { + expect(result.getByTestId('comboBoxSearchInput')).not.toBeDisabled(); + }); + + await act(async () => { + await userEvent.type(result.getByTestId('comboBoxSearchInput'), 'dr', { delay: 1 }); + }); + + await waitFor(() => { + expect( + result.getByTestId('comboBoxOptionsList createCaseAssigneesComboBox-optionsList') + ).toBeInTheDocument(); + }); + + await waitFor(async () => { + expect(result.getByText(`${currentUserProfile.user.full_name}`)).toBeInTheDocument(); + }); + + act(() => { + userEvent.click(result.getByText(`${currentUserProfile.user.full_name}`)); + }); + + await waitFor(() => { + expect(globalForm.getFormData()).toEqual({ assignees: [{ uid: currentUserProfile.uid }] }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/create/assignees.tsx b/x-pack/plugins/cases/public/components/create/assignees.tsx new file mode 100644 index 00000000000000..be4efc0edfed9a --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/assignees.tsx @@ -0,0 +1,225 @@ +/* + * 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 { isEmpty } from 'lodash'; +import React, { memo, useCallback, useState } from 'react'; +import { + EuiComboBox, + EuiComboBoxOptionOption, + EuiFormRow, + EuiLink, + EuiSelectableListItem, + EuiTextColor, +} from '@elastic/eui'; +import { + UserProfileWithAvatar, + UserAvatar, + getUserDisplayName, + UserProfile, +} from '@kbn/user-profile-components'; +import { UseField, FieldConfig, FieldHook } from '../../common/shared_imports'; +import { useSuggestUserProfiles } from '../../containers/user_profiles/use_suggest_user_profiles'; +import { useCasesContext } from '../cases_context/use_cases_context'; +import { useGetCurrentUserProfile } from '../../containers/user_profiles/use_get_current_user_profile'; +import { OptionalFieldLabel } from './optional_field_label'; +import * as i18n from './translations'; +import { bringCurrentUserToFrontAndSort } from '../user_profiles/sort'; +import { useAvailableCasesOwners } from '../app/use_available_owners'; +import { getAllPermissionsExceptFrom } from '../../utils/permissions'; + +interface Props { + isLoading: boolean; +} + +interface FieldProps { + field: FieldHook; + options: EuiComboBoxOptionOption[]; + isLoading: boolean; + isDisabled: boolean; + currentUserProfile?: UserProfile; + selectedOptions: EuiComboBoxOptionOption[]; + setSelectedOptions: React.Dispatch>; + onSearchComboChange: (value: string) => void; +} + +const getConfig = (): FieldConfig => ({ + label: i18n.ASSIGNEES, + defaultValue: [], +}); + +const userProfileToComboBoxOption = (userProfile: UserProfileWithAvatar) => ({ + label: getUserDisplayName(userProfile.user), + value: userProfile.uid, + user: userProfile.user, + data: userProfile.data, +}); + +const comboBoxOptionToAssignee = (option: EuiComboBoxOptionOption) => ({ uid: option.value }); + +const AssigneesFieldComponent: React.FC = React.memo( + ({ + field, + isLoading, + isDisabled, + options, + currentUserProfile, + selectedOptions, + setSelectedOptions, + onSearchComboChange, + }) => { + const { setValue } = field; + + const onComboChange = useCallback( + (currentOptions: EuiComboBoxOptionOption[]) => { + setSelectedOptions(currentOptions); + setValue(currentOptions.map((option) => comboBoxOptionToAssignee(option))); + }, + [setSelectedOptions, setValue] + ); + + const onSelfAssign = useCallback(() => { + if (!currentUserProfile) { + return; + } + + setSelectedOptions((prev) => [ + ...(prev ?? []), + userProfileToComboBoxOption(currentUserProfile), + ]); + + setValue([ + ...(selectedOptions?.map((option) => comboBoxOptionToAssignee(option)) ?? []), + { uid: currentUserProfile.uid }, + ]); + }, [currentUserProfile, selectedOptions, setSelectedOptions, setValue]); + + const renderOption = useCallback( + (option: EuiComboBoxOptionOption, searchValue: string, contentClassName: string) => { + const { user, data, value } = option as EuiComboBoxOptionOption & + UserProfileWithAvatar; + + return ( + } + className={contentClassName} + append={{user.email}} + > + {getUserDisplayName(user)} + + ); + }, + [] + ); + + const isCurrentUserSelected = Boolean( + selectedOptions?.find((option) => option.value === currentUserProfile?.uid) + ); + + return ( + + {i18n.ASSIGN_YOURSELF} + + ) : undefined + } + > + + + ); + } +); + +AssigneesFieldComponent.displayName = 'AssigneesFieldComponent'; + +const AssigneesComponent: React.FC = ({ isLoading: isLoadingForm }) => { + const { owner: owners } = useCasesContext(); + const availableOwners = useAvailableCasesOwners(getAllPermissionsExceptFrom('delete')); + const [searchTerm, setSearchTerm] = useState(''); + const [selectedOptions, setSelectedOptions] = useState(); + const [isUserTyping, setIsUserTyping] = useState(false); + const hasOwners = owners.length > 0; + + const { data: currentUserProfile, isLoading: isLoadingCurrentUserProfile } = + useGetCurrentUserProfile(); + + const onDebounce = useCallback(() => setIsUserTyping(false), []); + + const { + data: userProfiles, + isLoading: isLoadingSuggest, + isFetching: isFetchingSuggest, + } = useSuggestUserProfiles({ + name: searchTerm, + owners: hasOwners ? owners : availableOwners, + onDebounce, + }); + + const options = + bringCurrentUserToFrontAndSort(currentUserProfile, userProfiles)?.map((userProfile) => + userProfileToComboBoxOption(userProfile) + ) ?? []; + + const onSearchComboChange = (value: string) => { + if (!isEmpty(value)) { + setSearchTerm(value); + setIsUserTyping(true); + } + }; + + const isLoading = + isLoadingForm || + isLoadingCurrentUserProfile || + isLoadingSuggest || + isFetchingSuggest || + isUserTyping; + + const isDisabled = isLoadingForm || isLoadingCurrentUserProfile; + + return ( + + ); +}; + +AssigneesComponent.displayName = 'AssigneesComponent'; + +export const Assignees = memo(AssigneesComponent); diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx index 78f5a4e9d5c54d..d34e216c34af93 100644 --- a/x-pack/plugins/cases/public/components/create/form.tsx +++ b/x-pack/plugins/cases/public/components/create/form.tsx @@ -36,6 +36,7 @@ import { useCasesContext } from '../cases_context/use_cases_context'; import { useAvailableCasesOwners } from '../app/use_available_owners'; import { CaseAttachmentsWithoutOwner } from '../../types'; import { Severity } from './severity'; +import { Assignees } from './assignees'; interface ContainerProps { big?: boolean; @@ -86,6 +87,9 @@ export const CreateCaseFormFields: React.FC = React.m children: ( <> + <Container> + <Assignees isLoading={isSubmitting} /> + </Container> <Container> <Tags isLoading={isSubmitting} /> </Container> diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx index 6d8520e61a4938..8b45258889e5c0 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -6,14 +6,12 @@ */ import React from 'react'; -import { mount, ReactWrapper } from 'enzyme'; import { act, RenderResult, waitFor, within } from '@testing-library/react'; import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; -import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { CaseSeverity, CommentType, ConnectorTypes } from '../../../common/api'; import { useKibana } from '../../common/lib/kibana'; -import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock'; +import { AppMockRenderer, createAppMockRenderer } from '../../common/mock'; import { usePostCase } from '../../containers/use_post_case'; import { useCreateAttachments } from '../../containers/use_create_attachments'; import { useCaseConfigure } from '../../containers/configure/use_configure'; @@ -43,6 +41,8 @@ import { connectorsMock } from '../../common/mock/connectors'; import { CaseAttachments } from '../../types'; import { useGetConnectors } from '../../containers/configure/use_connectors'; import { useGetTags } from '../../containers/use_get_tags'; +import { waitForComponentToUpdate } from '../../common/test_utils'; +import { userProfiles } from '../../containers/user_profiles/api.mock'; const sampleId = 'case-id'; @@ -60,6 +60,7 @@ jest.mock('../connectors/jira/use_get_single_issue'); jest.mock('../connectors/jira/use_get_issues'); jest.mock('../connectors/servicenow/use_get_choices'); jest.mock('../../common/lib/kibana'); +jest.mock('../../containers/user_profiles/api'); const useGetConnectorsMock = useGetConnectors as jest.Mock; const useCaseConfigureMock = useCaseConfigure as jest.Mock; @@ -93,37 +94,21 @@ const defaultPostPushToService = { pushCaseToExternalService, }; -const fillForm = (wrapper: ReactWrapper) => { - wrapper - .find(`[data-test-subj="caseTitle"] input`) - .first() - .simulate('change', { target: { value: sampleData.title } }); - - wrapper - .find(`[data-test-subj="caseDescription"] textarea`) - .first() - .simulate('change', { target: { value: sampleData.description } }); - - act(() => { - ( - wrapper.find(EuiComboBox).props() as unknown as { - onChange: (a: EuiComboBoxOptionOption[]) => void; - } - ).onChange(sampleTags.map((tag) => ({ label: tag }))); - }); -}; - const fillFormReactTestingLib = async (renderResult: RenderResult) => { const titleInput = within(renderResult.getByTestId('caseTitle')).getByTestId('input'); + userEvent.type(titleInput, sampleData.title); const descriptionInput = renderResult.container.querySelector( `[data-test-subj="caseDescription"] textarea` ); + if (descriptionInput) { userEvent.type(descriptionInput, sampleData.description); } + const caseTags = renderResult.getByTestId('caseTags'); + for (let i = 0; i < sampleTags.length; i++) { const tagsInput = await within(caseTags).findByTestId('comboBoxInput'); userEvent.type(tagsInput, `${sampleTags[i]}{enter}`); @@ -185,6 +170,7 @@ describe('Create case', () => { expect(renderResult.getByTestId('caseTitle')).toBeTruthy(); expect(renderResult.getByTestId('caseSeverity')).toBeTruthy(); expect(renderResult.getByTestId('caseDescription')).toBeTruthy(); + expect(renderResult.getByTestId('createCaseAssigneesComboBox')).toBeTruthy(); expect(renderResult.getByTestId('caseTags')).toBeTruthy(); expect(renderResult.getByTestId('caseConnectors')).toBeTruthy(); expect(renderResult.getByTestId('case-creation-form-steps')).toBeTruthy(); @@ -241,31 +227,30 @@ describe('Create case', () => { it('does not submits the title when the length is longer than 64 characters', async () => { const longTitle = - 'This is a title that should not be saved as it is longer than 64 characters.'; - - const wrapper = mount( - <TestProviders> - <FormContext onSuccess={onFormSubmitSuccess}> - <CreateCaseFormFields {...defaultCreateCaseForm} /> - <SubmitCaseButton /> - </FormContext> - </TestProviders> + 'This is a title that should not be saved as it is longer than 64 characters.{enter}'; + + const renderResult = mockedContext.render( + <FormContext onSuccess={onFormSubmitSuccess}> + <CreateCaseFormFields {...defaultCreateCaseForm} /> + <SubmitCaseButton /> + </FormContext> ); + await act(async () => { + const titleInput = within(renderResult.getByTestId('caseTitle')).getByTestId('input'); + await userEvent.type(titleInput, longTitle, { delay: 1 }); + }); + act(() => { - wrapper - .find(`[data-test-subj="caseTitle"] input`) - .first() - .simulate('change', { target: { value: longTitle } }); - wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + userEvent.click(renderResult.getByTestId('create-case-submit')); }); await waitFor(() => { - wrapper.update(); - expect(wrapper.find('[data-test-subj="caseTitle"] .euiFormErrorText').text()).toBe( - 'The length of the title is too long. The maximum length is 64.' - ); + expect( + renderResult.getByText('The length of the title is too long. The maximum length is 64.') + ).toBeInTheDocument(); }); + expect(postCase).not.toHaveBeenCalled(); }); @@ -275,18 +260,25 @@ describe('Create case', () => { data: connectorsMock, }); - const wrapper = mount( - <TestProviders> - <FormContext onSuccess={onFormSubmitSuccess}> - <CreateCaseFormFields {...defaultCreateCaseForm} /> - <SubmitCaseButton /> - </FormContext> - </TestProviders> + const renderResult = mockedContext.render( + <FormContext onSuccess={onFormSubmitSuccess}> + <CreateCaseFormFields {...defaultCreateCaseForm} /> + <SubmitCaseButton /> + </FormContext> ); - fillForm(wrapper); - wrapper.find('[data-test-subj="caseSyncAlerts"] button').first().simulate('click'); - wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + await fillFormReactTestingLib(renderResult); + + act(() => { + const syncAlertsButton = within(renderResult.getByTestId('caseSyncAlerts')).getByTestId( + 'input' + ); + userEvent.click(syncAlertsButton); + }); + + act(() => { + userEvent.click(renderResult.getByTestId('create-case-submit')); + }); await waitFor(() => expect(postCase).toBeCalledWith({ ...sampleData, settings: { syncAlerts: false } }) @@ -294,22 +286,25 @@ describe('Create case', () => { }); it('should set sync alerts to false when the sync feature setting is false', async () => { + mockedContext = createAppMockRenderer({ + features: { alerts: { sync: false, enabled: true } }, + }); useGetConnectorsMock.mockReturnValue({ ...sampleConnectorData, data: connectorsMock, }); - const wrapper = mount( - <TestProviders features={{ alerts: { sync: false, enabled: true } }}> - <FormContext onSuccess={onFormSubmitSuccess}> - <CreateCaseFormFields {...defaultCreateCaseForm} /> - <SubmitCaseButton /> - </FormContext> - </TestProviders> + const renderResult = mockedContext.render( + <FormContext onSuccess={onFormSubmitSuccess}> + <CreateCaseFormFields {...defaultCreateCaseForm} /> + <SubmitCaseButton /> + </FormContext> ); - fillForm(wrapper); - wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + await fillFormReactTestingLib(renderResult); + act(() => { + userEvent.click(renderResult.getByTestId('create-case-submit')); + }); await waitFor(() => expect(postCase).toBeCalledWith({ ...sampleData, settings: { syncAlerts: false } }) @@ -345,18 +340,16 @@ describe('Create case', () => { data: connectorsMock, }); - const wrapper = mount( - <TestProviders> - <FormContext onSuccess={onFormSubmitSuccess}> - <CreateCaseFormFields {...defaultCreateCaseForm} /> - <SubmitCaseButton /> - </FormContext> - </TestProviders> + const renderResult = mockedContext.render( + <FormContext onSuccess={onFormSubmitSuccess}> + <CreateCaseFormFields {...defaultCreateCaseForm} /> + <SubmitCaseButton /> + </FormContext> ); - fillForm(wrapper); - await act(async () => { - wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + await fillFormReactTestingLib(renderResult); + act(() => { + userEvent.click(renderResult.getByTestId('create-case-submit')); }); await waitFor(() => @@ -395,17 +388,17 @@ describe('Create case', () => { data: connectorsMock, }); - const wrapper = mount( - <TestProviders> - <FormContext onSuccess={onFormSubmitSuccess}> - <CreateCaseFormFields {...defaultCreateCaseForm} /> - <SubmitCaseButton /> - </FormContext> - </TestProviders> + const renderResult = mockedContext.render( + <FormContext onSuccess={onFormSubmitSuccess}> + <CreateCaseFormFields {...defaultCreateCaseForm} /> + <SubmitCaseButton /> + </FormContext> ); - fillForm(wrapper); - wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + await fillFormReactTestingLib(renderResult); + act(() => { + userEvent.click(renderResult.getByTestId('create-case-submit')); + }); await waitFor(() => { expect(postCase).toBeCalledWith(sampleData); expect(pushCaseToExternalService).not.toHaveBeenCalled(); @@ -420,40 +413,43 @@ describe('Create case', () => { data: connectorsMock, }); - const wrapper = mount( - <TestProviders> - <FormContext onSuccess={onFormSubmitSuccess}> - <CreateCaseFormFields {...defaultCreateCaseForm} /> - <SubmitCaseButton /> - </FormContext> - </TestProviders> + const renderResult = mockedContext.render( + <FormContext onSuccess={onFormSubmitSuccess}> + <CreateCaseFormFields {...defaultCreateCaseForm} /> + <SubmitCaseButton /> + </FormContext> ); - fillForm(wrapper); - expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeFalsy(); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.find(`button[data-test-subj="dropdown-connector-jira-1"]`).simulate('click'); + await fillFormReactTestingLib(renderResult); + + act(() => { + userEvent.click(renderResult.getByTestId('dropdown-connectors')); + }); await waitFor(() => { - wrapper.update(); - expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeTruthy(); + expect(renderResult.getByTestId('dropdown-connector-jira-1')).toBeInTheDocument(); }); - wrapper - .find('select[data-test-subj="issueTypeSelect"]') - .first() - .simulate('change', { - target: { value: '10007' }, - }); + act(() => { + userEvent.click(renderResult.getByTestId('dropdown-connector-jira-1')); + }); - wrapper - .find('select[data-test-subj="prioritySelect"]') - .first() - .simulate('change', { - target: { value: '2' }, - }); + await waitFor(() => { + expect(renderResult.getByTestId('issueTypeSelect')).toBeInTheDocument(); + expect(renderResult.getByTestId('prioritySelect')).toBeInTheDocument(); + }); - wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + act(() => { + userEvent.selectOptions(renderResult.getByTestId('issueTypeSelect'), ['10007']); + }); + + act(() => { + userEvent.selectOptions(renderResult.getByTestId('prioritySelect'), ['Low']); + }); + + act(() => { + userEvent.click(renderResult.getByTestId('create-case-submit')); + }); await waitFor(() => { expect(postCase).toBeCalledWith({ @@ -462,7 +458,7 @@ describe('Create case', () => { id: 'jira-1', name: 'Jira', type: '.jira', - fields: { issueType: '10007', parent: null, priority: '2' }, + fields: { issueType: '10007', parent: null, priority: 'Low' }, }, }); expect(pushCaseToExternalService).toHaveBeenCalledWith({ @@ -471,7 +467,7 @@ describe('Create case', () => { id: 'jira-1', name: 'Jira', type: '.jira', - fields: { issueType: '10007', parent: null, priority: '2' }, + fields: { issueType: '10007', parent: null, priority: 'Low' }, }, }); expect(onFormSubmitSuccess).toHaveBeenCalledWith({ @@ -487,41 +483,49 @@ describe('Create case', () => { data: connectorsMock, }); - const wrapper = mount( - <TestProviders> - <FormContext onSuccess={onFormSubmitSuccess}> - <CreateCaseFormFields {...defaultCreateCaseForm} /> - <SubmitCaseButton /> - </FormContext> - </TestProviders> + const renderResult = mockedContext.render( + <FormContext onSuccess={onFormSubmitSuccess}> + <CreateCaseFormFields {...defaultCreateCaseForm} /> + <SubmitCaseButton /> + </FormContext> ); - fillForm(wrapper); - expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeFalsy(); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.find(`button[data-test-subj="dropdown-connector-resilient-2"]`).simulate('click'); + await fillFormReactTestingLib(renderResult); + + act(() => { + userEvent.click(renderResult.getByTestId('dropdown-connectors')); + }); await waitFor(() => { - wrapper.update(); - expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeTruthy(); + expect(renderResult.getByTestId('dropdown-connector-resilient-2')).toBeInTheDocument(); }); act(() => { - ( - wrapper.find(EuiComboBox).at(1).props() as unknown as { - onChange: (a: EuiComboBoxOptionOption[]) => void; - } - ).onChange([{ value: '19', label: 'Denial of Service' }]); - }); - - wrapper - .find('select[data-test-subj="severitySelect"]') - .first() - .simulate('change', { - target: { value: '4' }, + userEvent.click(renderResult.getByTestId('dropdown-connector-resilient-2')); + }); + + await waitFor(() => { + expect(renderResult.getByTestId('incidentTypeComboBox')).toBeInTheDocument(); + expect(renderResult.getByTestId('severitySelect')).toBeInTheDocument(); + }); + + const checkbox = within(renderResult.getByTestId('incidentTypeComboBox')).getByTestId( + 'comboBoxSearchInput' + ); + + await act(async () => { + await userEvent.type(checkbox, 'Denial of Service{enter}', { + delay: 2, }); + }); - wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + act(() => { + userEvent.selectOptions(renderResult.getByTestId('severitySelect'), ['4']); + }); + + act(() => { + userEvent.click(renderResult.getByTestId('create-case-submit')); + }); await waitFor(() => { expect(postCase).toBeCalledWith({ @@ -530,7 +534,7 @@ describe('Create case', () => { id: 'resilient-2', name: 'My Connector 2', type: '.resilient', - fields: { incidentTypes: ['19'], severityCode: '4' }, + fields: { incidentTypes: ['21'], severityCode: '4' }, }, }); @@ -540,7 +544,7 @@ describe('Create case', () => { id: 'resilient-2', name: 'My Connector 2', type: '.resilient', - fields: { incidentTypes: ['19'], severityCode: '4' }, + fields: { incidentTypes: ['21'], severityCode: '4' }, }, }); @@ -557,54 +561,53 @@ describe('Create case', () => { data: connectorsMock, }); - const wrapper = mount( - <TestProviders> - <FormContext onSuccess={onFormSubmitSuccess}> - <CreateCaseFormFields {...defaultCreateCaseForm} /> - <SubmitCaseButton /> - </FormContext> - </TestProviders> + const renderResult = mockedContext.render( + <FormContext onSuccess={onFormSubmitSuccess}> + <CreateCaseFormFields {...defaultCreateCaseForm} /> + <SubmitCaseButton /> + </FormContext> ); - fillForm(wrapper); - expect(wrapper.find(`[data-test-subj="connector-fields-sn-itsm"]`).exists()).toBeFalsy(); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.find(`button[data-test-subj="dropdown-connector-servicenow-1"]`).simulate('click'); + await fillFormReactTestingLib(renderResult); + + act(() => { + userEvent.click(renderResult.getByTestId('dropdown-connectors')); + }); await waitFor(() => { - wrapper.update(); - expect(wrapper.find(`[data-test-subj="connector-fields-sn-itsm"]`).exists()).toBeTruthy(); + expect(renderResult.getByTestId('dropdown-connector-servicenow-1')).toBeInTheDocument(); }); - // we need the choices response to conditionally show the subcategory select + act(() => { + userEvent.click(renderResult.getByTestId('dropdown-connector-servicenow-1')); + }); + + await waitFor(() => { + expect(onChoicesSuccess).toBeDefined(); + }); + + // // we need the choices response to conditionally show the subcategory select act(() => { onChoicesSuccess(useGetChoicesResponse.choices); }); ['severitySelect', 'urgencySelect', 'impactSelect'].forEach((subj) => { - wrapper - .find(`select[data-test-subj="${subj}"]`) - .first() - .simulate('change', { - target: { value: '2' }, - }); - }); - - wrapper - .find('select[data-test-subj="categorySelect"]') - .first() - .simulate('change', { - target: { value: 'software' }, + act(() => { + userEvent.selectOptions(renderResult.getByTestId(subj), ['2']); }); + }); - wrapper - .find('select[data-test-subj="subcategorySelect"]') - .first() - .simulate('change', { - target: { value: 'os' }, - }); + act(() => { + userEvent.selectOptions(renderResult.getByTestId('categorySelect'), ['software']); + }); - wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + act(() => { + userEvent.selectOptions(renderResult.getByTestId('subcategorySelect'), ['os']); + }); + + act(() => { + userEvent.click(renderResult.getByTestId('create-case-submit')); + }); await waitFor(() => { expect(postCase).toBeCalledWith({ @@ -652,23 +655,29 @@ describe('Create case', () => { data: connectorsMock, }); - const wrapper = mount( - <TestProviders> - <FormContext onSuccess={onFormSubmitSuccess}> - <CreateCaseFormFields {...defaultCreateCaseForm} /> - <SubmitCaseButton /> - </FormContext> - </TestProviders> + const renderResult = mockedContext.render( + <FormContext onSuccess={onFormSubmitSuccess}> + <CreateCaseFormFields {...defaultCreateCaseForm} /> + <SubmitCaseButton /> + </FormContext> ); - fillForm(wrapper); - expect(wrapper.find(`[data-test-subj="connector-fields-sn-sir"]`).exists()).toBeFalsy(); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.find(`button[data-test-subj="dropdown-connector-servicenow-sir"]`).simulate('click'); + await fillFormReactTestingLib(renderResult); + + act(() => { + userEvent.click(renderResult.getByTestId('dropdown-connectors')); + }); + + await waitFor(() => { + expect(renderResult.getByTestId('dropdown-connector-servicenow-sir')).toBeInTheDocument(); + }); + + act(() => { + userEvent.click(renderResult.getByTestId('dropdown-connector-servicenow-sir')); + }); await waitFor(() => { - wrapper.update(); - expect(wrapper.find(`[data-test-subj="connector-fields-sn-sir"]`).exists()).toBeTruthy(); + expect(onChoicesSuccess).toBeDefined(); }); // we need the choices response to conditionally show the subcategory select @@ -676,33 +685,25 @@ describe('Create case', () => { onChoicesSuccess(useGetChoicesResponse.choices); }); - wrapper - .find('[data-test-subj="destIpCheckbox"] input') - .first() - .simulate('change', { target: { checked: false } }); + act(() => { + userEvent.click(renderResult.getByTestId('destIpCheckbox')); + }); - wrapper - .find('select[data-test-subj="prioritySelect"]') - .first() - .simulate('change', { - target: { value: '1' }, - }); + act(() => { + userEvent.selectOptions(renderResult.getByTestId('prioritySelect'), ['1']); + }); - wrapper - .find('select[data-test-subj="categorySelect"]') - .first() - .simulate('change', { - target: { value: 'Denial of Service' }, - }); + act(() => { + userEvent.selectOptions(renderResult.getByTestId('categorySelect'), ['Denial of Service']); + }); - wrapper - .find('select[data-test-subj="subcategorySelect"]') - .first() - .simulate('change', { - target: { value: '26' }, - }); + act(() => { + userEvent.selectOptions(renderResult.getByTestId('subcategorySelect'), ['26']); + }); - wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + act(() => { + userEvent.click(renderResult.getByTestId('create-case-submit')); + }); await waitFor(() => { expect(postCase).toBeCalledWith({ @@ -755,23 +756,31 @@ describe('Create case', () => { data: connectorsMock, }); - const wrapper = mockedContext.render( + const renderResult = mockedContext.render( <FormContext onSuccess={onFormSubmitSuccess} afterCaseCreated={afterCaseCreated}> <CreateCaseFormFields {...defaultCreateCaseForm} /> <SubmitCaseButton /> </FormContext> ); - await fillFormReactTestingLib(wrapper); - expect(wrapper.queryByTestId('connector-fields-jira')).toBeFalsy(); - userEvent.click(wrapper.getByTestId('dropdown-connectors')); - await waitForEuiPopoverOpen(); - await act(async () => { - userEvent.click(wrapper.getByTestId('dropdown-connector-jira-1')); + await fillFormReactTestingLib(renderResult); + + act(() => { + userEvent.click(renderResult.getByTestId('dropdown-connectors')); + }); + + await waitFor(() => { + expect(renderResult.getByTestId('dropdown-connector-jira-1')).toBeInTheDocument(); + }); + + act(() => { + userEvent.click(renderResult.getByTestId('dropdown-connector-jira-1')); + }); + + act(() => { + userEvent.click(renderResult.getByTestId('create-case-submit')); }); - expect(wrapper.getByTestId('connector-fields-jira')).toBeTruthy(); - userEvent.click(wrapper.getByTestId('create-case-submit')); await waitFor(() => { expect(afterCaseCreated).toHaveBeenCalledWith( { @@ -811,19 +820,21 @@ describe('Create case', () => { }, ]; - const wrapper = mockedContext.render( + const renderResult = mockedContext.render( <FormContext onSuccess={onFormSubmitSuccess} attachments={attachments}> <CreateCaseFormFields {...defaultCreateCaseForm} /> <SubmitCaseButton /> </FormContext> ); - await fillFormReactTestingLib(wrapper); + await fillFormReactTestingLib(renderResult); - await act(async () => { - userEvent.click(wrapper.getByTestId('create-case-submit')); + act(() => { + userEvent.click(renderResult.getByTestId('create-case-submit')); }); + await waitForComponentToUpdate(); + expect(createAttachments).toHaveBeenCalledTimes(1); expect(createAttachments).toHaveBeenCalledWith({ caseId: 'case-id', @@ -839,19 +850,21 @@ describe('Create case', () => { }); const attachments: CaseAttachments = []; - const wrapper = mockedContext.render( + const renderResult = mockedContext.render( <FormContext onSuccess={onFormSubmitSuccess} attachments={attachments}> <CreateCaseFormFields {...defaultCreateCaseForm} /> <SubmitCaseButton /> </FormContext> ); - await fillFormReactTestingLib(wrapper); + await fillFormReactTestingLib(renderResult); - await act(async () => { - userEvent.click(wrapper.getByTestId('create-case-submit')); + act(() => { + userEvent.click(renderResult.getByTestId('create-case-submit')); }); + await waitForComponentToUpdate(); + expect(createAttachments).not.toHaveBeenCalled(); }); @@ -873,30 +886,35 @@ describe('Create case', () => { }, ]; - const wrapper = mount( - <TestProviders> - <FormContext - onSuccess={onFormSubmitSuccess} - afterCaseCreated={afterCaseCreated} - attachments={attachments} - > - <CreateCaseFormFields {...defaultCreateCaseForm} /> - <SubmitCaseButton /> - </FormContext> - </TestProviders> + const renderResult = mockedContext.render( + <FormContext + onSuccess={onFormSubmitSuccess} + afterCaseCreated={afterCaseCreated} + attachments={attachments} + > + <CreateCaseFormFields {...defaultCreateCaseForm} /> + <SubmitCaseButton /> + </FormContext> ); - fillForm(wrapper); - expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeFalsy(); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.find(`button[data-test-subj="dropdown-connector-jira-1"]`).simulate('click'); + await fillFormReactTestingLib(renderResult); + + act(() => { + userEvent.click(renderResult.getByTestId('dropdown-connectors')); + }); await waitFor(() => { - wrapper.update(); - expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeTruthy(); + expect(renderResult.getByTestId('dropdown-connector-jira-1')).toBeInTheDocument(); + }); + + act(() => { + userEvent.click(renderResult.getByTestId('dropdown-connector-jira-1')); + }); + + act(() => { + userEvent.click(renderResult.getByTestId('create-case-submit')); }); - wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); await waitFor(() => { expect(postCase).toHaveBeenCalled(); expect(createAttachments).toHaveBeenCalled(); @@ -933,9 +951,7 @@ describe('Create case', () => { </FormContext> ); - await act(async () => { - fillFormReactTestingLib(result); - }); + await fillFormReactTestingLib(result); await act(async () => { userEvent.click(result.getByTestId('create-case-submit')); @@ -944,4 +960,54 @@ describe('Create case', () => { expect(pushCaseToExternalService).not.toHaveBeenCalled(); }); }); + + describe('Assignees', () => { + it('should submit assignees', async () => { + const renderResult = mockedContext.render( + <FormContext onSuccess={onFormSubmitSuccess}> + <CreateCaseFormFields {...defaultCreateCaseForm} /> + <SubmitCaseButton /> + </FormContext> + ); + + await fillFormReactTestingLib(renderResult); + + const assigneesComboBox = within(renderResult.getByTestId('createCaseAssigneesComboBox')); + + await waitFor(() => { + expect(assigneesComboBox.getByTestId('comboBoxSearchInput')).not.toBeDisabled(); + }); + + await act(async () => { + await userEvent.type(assigneesComboBox.getByTestId('comboBoxSearchInput'), 'dr', { + delay: 1, + }); + }); + + await waitFor(() => { + expect( + renderResult.getByTestId('comboBoxOptionsList createCaseAssigneesComboBox-optionsList') + ).toBeInTheDocument(); + }); + + await waitFor(async () => { + expect(renderResult.getByText(`${userProfiles[0].user.full_name}`)).toBeInTheDocument(); + }); + + act(() => { + userEvent.click(renderResult.getByText(`${userProfiles[0].user.full_name}`)); + }); + + act(() => { + userEvent.click(renderResult.getByTestId('create-case-submit')); + }); + + await waitForComponentToUpdate(); + + expect(postCase).toBeCalledWith({ + ...sampleData, + assignees: [{ uid: userProfiles[0].uid }], + }); + }); + }); }); diff --git a/x-pack/plugins/cases/public/components/create/index.test.tsx b/x-pack/plugins/cases/public/components/create/index.test.tsx index c455ae0c3628de..ee2d2d9a4468c5 100644 --- a/x-pack/plugins/cases/public/components/create/index.test.tsx +++ b/x-pack/plugins/cases/public/components/create/index.test.tsx @@ -31,6 +31,7 @@ import { useGetConnectors } from '../../containers/configure/use_connectors'; import { useGetTags } from '../../containers/use_get_tags'; jest.mock('../../containers/api'); +jest.mock('../../containers/user_profiles/api'); jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/configure/use_connectors'); jest.mock('../../containers/configure/use_configure'); @@ -63,7 +64,7 @@ const fillForm = (wrapper: ReactWrapper) => { act(() => { ( - wrapper.find(EuiComboBox).props() as unknown as { + wrapper.find(EuiComboBox).at(1).props() as unknown as { onChange: (a: EuiComboBoxOptionOption[]) => void; } ).onChange(sampleTags.map((tag) => ({ label: tag }))); diff --git a/x-pack/plugins/cases/public/components/create/mock.ts b/x-pack/plugins/cases/public/components/create/mock.ts index 8f67b3c05d3e41..c54e3206b2b016 100644 --- a/x-pack/plugins/cases/public/components/create/mock.ts +++ b/x-pack/plugins/cases/public/components/create/mock.ts @@ -25,6 +25,7 @@ export const sampleData: CasePostRequest = { syncAlerts: true, }, owner: SECURITY_SOLUTION_OWNER, + assignees: [], }; export const sampleConnectorData = { isLoading: false, data: [] }; diff --git a/x-pack/plugins/cases/public/components/create/schema.tsx b/x-pack/plugins/cases/public/components/create/schema.tsx index d72b1cc523f0df..59cf8f919606b1 100644 --- a/x-pack/plugins/cases/public/components/create/schema.tsx +++ b/x-pack/plugins/cases/public/components/create/schema.tsx @@ -100,4 +100,5 @@ export const schema: FormSchema<FormProps> = { type: FIELD_TYPES.TOGGLE, defaultValue: true, }, + assignees: {}, }; diff --git a/x-pack/plugins/cases/public/components/create/translations.ts b/x-pack/plugins/cases/public/components/create/translations.ts index 7e0f7e5a6b9d58..780a1bbd1d02fc 100644 --- a/x-pack/plugins/cases/public/components/create/translations.ts +++ b/x-pack/plugins/cases/public/components/create/translations.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; export * from '../../common/translations'; +export * from '../user_profiles/translations'; export const STEP_ONE_TITLE = i18n.translate('xpack.cases.create.stepOneTitle', { defaultMessage: 'Case fields', @@ -24,3 +25,7 @@ export const STEP_THREE_TITLE = i18n.translate('xpack.cases.create.stepThreeTitl export const SYNC_ALERTS_LABEL = i18n.translate('xpack.cases.create.syncAlertsLabel', { defaultMessage: 'Sync alert status with case status', }); + +export const ASSIGN_YOURSELF = i18n.translate('xpack.cases.create.assignYourself', { + defaultMessage: 'Assign yourself', +}); diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx index 3d7be7f08084d0..5db400203468a0 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx @@ -219,21 +219,13 @@ describe('EditConnector ', () => { expect(wrapper.find(`[data-test-subj="has-data-to-push-button"]`).exists()).toBeFalsy(); }); - it('displays the callout message when none is selected', async () => { + it('display the callout message when none is selected', async () => { const defaultProps = getDefaultProps(); const props = { ...defaultProps, connectors: [] }; - const wrapper = mount( - <TestProviders> - <EditConnector {...props} /> - </TestProviders> - ); - wrapper.update(); - await waitFor(() => { - expect(true).toBeTruthy(); - }); - wrapper.update(); + const result = appMockRender.render(<EditConnector {...props} />); + await waitFor(() => { - expect(wrapper.find(`[data-test-subj="push-callouts"]`).exists()).toEqual(true); + expect(result.getByTestId('push-callouts')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/cases/public/components/filter_popover/index.tsx b/x-pack/plugins/cases/public/components/filter_popover/index.tsx index bd4f665bcd7fd4..0ef1d5a887c21b 100644 --- a/x-pack/plugins/cases/public/components/filter_popover/index.tsx +++ b/x-pack/plugins/cases/public/components/filter_popover/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Dispatch, SetStateAction, useCallback, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { EuiFilterButton, EuiFilterSelectItem, @@ -19,7 +19,7 @@ import styled from 'styled-components'; interface FilterPopoverProps { buttonLabel: string; - onSelectedOptionsChanged: Dispatch<SetStateAction<string[]>>; + onSelectedOptionsChanged: (value: string[]) => void; options: string[]; optionsEmptyLabel?: string; selectedOptions: string[]; diff --git a/x-pack/plugins/cases/public/components/header_page/__snapshots__/editable_title.test.tsx.snap b/x-pack/plugins/cases/public/components/header_page/__snapshots__/editable_title.test.tsx.snap deleted file mode 100644 index 73f466aeec7710..00000000000000 --- a/x-pack/plugins/cases/public/components/header_page/__snapshots__/editable_title.test.tsx.snap +++ /dev/null @@ -1,98 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EditableTitle renders 1`] = ` -<I18nProvider> - <Component> - <ThemeProvider - theme={[Function]} - > - <QueryClientProvider - client={ - QueryClient { - "defaultOptions": Object { - "queries": Object { - "retry": false, - }, - }, - "logger": BufferedConsole { - "Console": [Function], - "_buffer": Array [], - "_counters": Object {}, - "_groupDepth": 0, - "_timers": Object {}, - "assert": [Function], - "clear": [Function], - "count": [Function], - "countReset": [Function], - "debug": [Function], - "dir": [Function], - "dirxml": [Function], - "error": [Function], - "group": [Function], - "groupCollapsed": [Function], - "groupEnd": [Function], - "info": [Function], - "log": [Function], - "table": [Function], - "time": [Function], - "timeEnd": [Function], - "timeLog": [Function], - "trace": [Function], - "warn": [Function], - }, - "mutationCache": MutationCache { - "config": Object {}, - "listeners": Array [], - "mutationId": 0, - "mutations": Array [], - "subscribe": [Function], - }, - "mutationDefaults": Array [], - "queryCache": QueryCache { - "config": Object {}, - "listeners": Array [], - "queries": Array [], - "queriesMap": Object {}, - "subscribe": [Function], - }, - "queryDefaults": Array [], - } - } - > - <CasesProvider - value={ - Object { - "externalReferenceAttachmentTypeRegistry": ExternalReferenceAttachmentTypeRegistry { - "collection": Map {}, - "name": "ExternalReferenceAttachmentTypeRegistry", - }, - "features": undefined, - "owner": Array [ - "securitySolution", - ], - "permissions": Object { - "all": true, - "create": true, - "delete": true, - "push": true, - "read": true, - "update": true, - }, - "persistableStateAttachmentTypeRegistry": PersistableStateAttachmentTypeRegistry { - "collection": Map {}, - "name": "PersistableStateAttachmentTypeRegistry", - }, - } - } - > - <Memo(EditableTitle) - isLoading={false} - onSubmit={[MockFunction]} - title="Test title" - /> - </CasesProvider> - </QueryClientProvider> - </ThemeProvider> - </Component> -</I18nProvider> -`; diff --git a/x-pack/plugins/cases/public/components/header_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/cases/public/components/header_page/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 7e6d9e2b05d94b..00000000000000 --- a/x-pack/plugins/cases/public/components/header_page/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,103 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`HeaderPage it renders 1`] = ` -<I18nProvider> - <Component> - <ThemeProvider - theme={[Function]} - > - <QueryClientProvider - client={ - QueryClient { - "defaultOptions": Object { - "queries": Object { - "retry": false, - }, - }, - "logger": BufferedConsole { - "Console": [Function], - "_buffer": Array [], - "_counters": Object {}, - "_groupDepth": 0, - "_timers": Object {}, - "assert": [Function], - "clear": [Function], - "count": [Function], - "countReset": [Function], - "debug": [Function], - "dir": [Function], - "dirxml": [Function], - "error": [Function], - "group": [Function], - "groupCollapsed": [Function], - "groupEnd": [Function], - "info": [Function], - "log": [Function], - "table": [Function], - "time": [Function], - "timeEnd": [Function], - "timeLog": [Function], - "trace": [Function], - "warn": [Function], - }, - "mutationCache": MutationCache { - "config": Object {}, - "listeners": Array [], - "mutationId": 0, - "mutations": Array [], - "subscribe": [Function], - }, - "mutationDefaults": Array [], - "queryCache": QueryCache { - "config": Object {}, - "listeners": Array [], - "queries": Array [], - "queriesMap": Object {}, - "subscribe": [Function], - }, - "queryDefaults": Array [], - } - } - > - <CasesProvider - value={ - Object { - "externalReferenceAttachmentTypeRegistry": ExternalReferenceAttachmentTypeRegistry { - "collection": Map {}, - "name": "ExternalReferenceAttachmentTypeRegistry", - }, - "features": undefined, - "owner": Array [ - "securitySolution", - ], - "permissions": Object { - "all": true, - "create": true, - "delete": true, - "push": true, - "read": true, - "update": true, - }, - "persistableStateAttachmentTypeRegistry": PersistableStateAttachmentTypeRegistry { - "collection": Map {}, - "name": "PersistableStateAttachmentTypeRegistry", - }, - } - } - > - <Memo(HeaderPage) - border={true} - subtitle="Test subtitle" - subtitle2="Test subtitle 2" - title="Test title" - > - <p> - Test supplement - </p> - </Memo(HeaderPage)> - </CasesProvider> - </QueryClientProvider> - </ThemeProvider> - </Component> -</I18nProvider> -`; diff --git a/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx b/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx index f36996c0134716..e2893cbbc5aa8d 100644 --- a/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx +++ b/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; import '../../common/mock/match_media'; @@ -27,18 +26,16 @@ describe('EditableTitle', () => { isLoading: false, }; + let appMock: AppMockRenderer; + beforeEach(() => { jest.clearAllMocks(); + appMock = createAppMockRenderer(); }); it('renders', () => { - const wrapper = shallow( - <TestProviders> - <EditableTitle {...defaultProps} /> - </TestProviders> - ); - - expect(wrapper).toMatchSnapshot(); + const renderResult = appMock.render(<EditableTitle {...defaultProps} />); + expect(renderResult.getByText('Test title')).toBeInTheDocument(); }); it('does not show the edit icon when the user does not have edit permissions', () => { @@ -269,12 +266,6 @@ describe('EditableTitle', () => { }); describe('Badges', () => { - let appMock: AppMockRenderer; - - beforeEach(() => { - appMock = createAppMockRenderer(); - }); - it('does not render the badge if the release is ga', () => { const renderResult = appMock.render(<EditableTitle {...defaultProps} />); diff --git a/x-pack/plugins/cases/public/components/header_page/index.test.tsx b/x-pack/plugins/cases/public/components/header_page/index.test.tsx index 707cb9b7c43352..c5c7ddcaab8759 100644 --- a/x-pack/plugins/cases/public/components/header_page/index.test.tsx +++ b/x-pack/plugins/cases/public/components/header_page/index.test.tsx @@ -6,7 +6,6 @@ */ import { euiDarkVars } from '@kbn/ui-theme'; -import { shallow } from 'enzyme'; import React from 'react'; import '../../common/mock/match_media'; @@ -18,9 +17,15 @@ jest.mock('../../common/navigation/hooks'); describe('HeaderPage', () => { const mount = useMountAppended(); + let appMock: AppMockRenderer; + + beforeEach(() => { + jest.clearAllMocks(); + appMock = createAppMockRenderer(); + }); test('it renders', () => { - const wrapper = shallow( + const result = appMock.render( <TestProviders> <HeaderPage border subtitle="Test subtitle" subtitle2="Test subtitle 2" title="Test title"> <p>{'Test supplement'}</p> @@ -28,7 +33,10 @@ describe('HeaderPage', () => { </TestProviders> ); - expect(wrapper).toMatchSnapshot(); + expect(result.getByText('Test subtitle')).toBeInTheDocument(); + expect(result.getByText('Test subtitle 2')).toBeInTheDocument(); + expect(result.getByText('Test title')).toBeInTheDocument(); + expect(result.getByText('Test supplement')).toBeInTheDocument(); }); test('it renders the back link when provided', () => { @@ -140,12 +148,6 @@ describe('HeaderPage', () => { }); describe('Badges', () => { - let appMock: AppMockRenderer; - - beforeEach(() => { - appMock = createAppMockRenderer(); - }); - it('does not render the badge if the release is ga', () => { const renderResult = appMock.render(<HeaderPage title="Test title" />); diff --git a/x-pack/plugins/cases/public/components/tag_list/tags.tsx b/x-pack/plugins/cases/public/components/tags/tags.tsx similarity index 99% rename from x-pack/plugins/cases/public/components/tag_list/tags.tsx rename to x-pack/plugins/cases/public/components/tags/tags.tsx index ec8a84de1aa884..dd27a4a91ca12c 100644 --- a/x-pack/plugins/cases/public/components/tag_list/tags.tsx +++ b/x-pack/plugins/cases/public/components/tags/tags.tsx @@ -14,9 +14,11 @@ interface TagsProps { color?: string; gutterSize?: EuiBadgeGroupProps['gutterSize']; } + const MyEuiBadge = styled(EuiBadge)` max-width: 200px; `; + const TagsComponent: React.FC<TagsProps> = ({ tags, color = 'default', gutterSize }) => ( <> {tags.length > 0 && ( diff --git a/x-pack/plugins/cases/public/components/tag_list/translations.ts b/x-pack/plugins/cases/public/components/tags/translations.ts similarity index 100% rename from x-pack/plugins/cases/public/components/tag_list/translations.ts rename to x-pack/plugins/cases/public/components/tags/translations.ts diff --git a/x-pack/plugins/cases/public/components/types.ts b/x-pack/plugins/cases/public/components/types.ts index d31c297d18b1c1..d9ba8890aab314 100644 --- a/x-pack/plugins/cases/public/components/types.ts +++ b/x-pack/plugins/cases/public/components/types.ts @@ -5,6 +5,10 @@ * 2.0. */ +import { UserProfileWithAvatar } from '@kbn/user-profile-components'; + export type { CaseActionConnector } from '../../common/ui/types'; export type ReleasePhase = 'experimental' | 'beta' | 'ga'; + +export type CurrentUserProfile = UserProfileWithAvatar | undefined; diff --git a/x-pack/plugins/cases/public/components/user_actions/assignees.test.tsx b/x-pack/plugins/cases/public/components/user_actions/assignees.test.tsx new file mode 100644 index 00000000000000..57cde43c9fee69 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/assignees.test.tsx @@ -0,0 +1,230 @@ +/* + * 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 from 'react'; +import { EuiCommentList } from '@elastic/eui'; +import { render, screen } from '@testing-library/react'; + +import { Actions } from '../../../common/api'; +import { elasticUser, getUserAction } from '../../containers/mock'; +import { TestProviders } from '../../common/mock'; +import { createAssigneesUserActionBuilder, shouldAddAnd, shouldAddComma } from './assignees'; +import { getMockBuilderArgs } from './mock'; + +jest.mock('../../common/lib/kibana'); +jest.mock('../../common/navigation/hooks'); + +describe('createAssigneesUserActionBuilder', () => { + describe('shouldAddComma', () => { + it('returns false if there are only 2 items', () => { + expect(shouldAddComma(0, 2)).toBeFalsy(); + }); + + it('returns false it is the last items', () => { + expect(shouldAddComma(2, 3)).toBeFalsy(); + }); + }); + + describe('shouldAddAnd', () => { + it('returns false if there is only 1 item', () => { + expect(shouldAddAnd(0, 1)).toBeFalsy(); + }); + + it('returns false it is not the last items', () => { + expect(shouldAddAnd(1, 3)).toBeFalsy(); + }); + }); + + describe('component', () => { + const builderArgs = getMockBuilderArgs(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders assigned users', () => { + const userAction = getUserAction('assignees', Actions.add, { + createdBy: { + // damaged_raccoon uid + profileUid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0', + }, + }); + const builder = createAssigneesUserActionBuilder({ + ...builderArgs, + userAction, + }); + + const createdUserAction = builder.build(); + render( + <TestProviders> + <EuiCommentList comments={createdUserAction} /> + </TestProviders> + ); + + expect(screen.getByText('assigned')).toBeInTheDocument(); + expect(screen.getByText('themselves')).toBeInTheDocument(); + expect(screen.getByText('Physical Dinosaur')).toBeInTheDocument(); + + expect(screen.getByTestId('ua-assignee-physical_dinosaur')).toContainElement( + screen.getByText('and') + ); + }); + + it('renders assigned users with a comma', () => { + const userAction = getUserAction('assignees', Actions.add, { + createdBy: { + // damaged_raccoon uid + profileUid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0', + }, + payload: { + assignees: [ + // These values map to uids in x-pack/plugins/cases/public/containers/user_profiles/api.mock.ts + { uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0' }, + { uid: 'u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0' }, + { uid: 'u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0' }, + ], + }, + }); + const builder = createAssigneesUserActionBuilder({ + ...builderArgs, + userAction, + }); + + const createdUserAction = builder.build(); + render( + <TestProviders> + <EuiCommentList comments={createdUserAction} /> + </TestProviders> + ); + + expect(screen.getByText('assigned')).toBeInTheDocument(); + expect(screen.getByText('themselves,')).toBeInTheDocument(); + expect(screen.getByText('Physical Dinosaur')).toBeInTheDocument(); + + expect(screen.getByTestId('ua-assignee-physical_dinosaur')).toContainElement( + screen.getByText(',') + ); + + expect(screen.getByText('Wet Dingo')).toBeInTheDocument(); + expect(screen.getByTestId('ua-assignee-wet_dingo')).toContainElement(screen.getByText('and')); + }); + + it('renders unassigned users', () => { + const userAction = getUserAction('assignees', Actions.delete, { + createdBy: { + // damaged_raccoon uid + profileUid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0', + }, + }); + const builder = createAssigneesUserActionBuilder({ + ...builderArgs, + userAction, + }); + + const createdUserAction = builder.build(); + render( + <TestProviders> + <EuiCommentList comments={createdUserAction} /> + </TestProviders> + ); + + expect(screen.getByText('unassigned')).toBeInTheDocument(); + expect(screen.getByText('themselves')).toBeInTheDocument(); + expect(screen.getByText('Physical Dinosaur')).toBeInTheDocument(); + + expect(screen.getByTestId('ua-assignee-physical_dinosaur')).toContainElement( + screen.getByText('and') + ); + }); + + it('renders a single assigned user', () => { + const userAction = getUserAction('assignees', Actions.add, { + payload: { + assignees: [ + // only render the physical dinosaur + { uid: 'u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0' }, + ], + }, + }); + const builder = createAssigneesUserActionBuilder({ + ...builderArgs, + userAction, + }); + + const createdUserAction = builder.build(); + render( + <TestProviders> + <EuiCommentList comments={createdUserAction} /> + </TestProviders> + ); + + expect(screen.getByText('Physical Dinosaur')).toBeInTheDocument(); + expect(screen.queryByText('themselves,')).not.toBeInTheDocument(); + expect(screen.queryByText('and')).not.toBeInTheDocument(); + }); + + it('renders a single assigned user that is themselves using matching profile uids', () => { + const userAction = getUserAction('assignees', Actions.add, { + createdBy: { + ...elasticUser, + profileUid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0', + }, + payload: { + assignees: [ + // only render the damaged raccoon which is the current user + { uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0' }, + ], + }, + }); + const builder = createAssigneesUserActionBuilder({ + ...builderArgs, + userAction, + }); + + const createdUserAction = builder.build(); + render( + <TestProviders> + <EuiCommentList comments={createdUserAction} /> + </TestProviders> + ); + + expect(screen.getByText('themselves')).toBeInTheDocument(); + expect(screen.queryByText('Physical Dinosaur')).not.toBeInTheDocument(); + expect(screen.queryByText('and')).not.toBeInTheDocument(); + }); + + it('renders a single assigned user that is themselves using matching usernames', () => { + const userAction = getUserAction('assignees', Actions.add, { + createdBy: { + ...elasticUser, + username: 'damaged_raccoon', + }, + payload: { + assignees: [ + // only render the damaged raccoon which is the current user + { uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0' }, + ], + }, + }); + const builder = createAssigneesUserActionBuilder({ + ...builderArgs, + userAction, + }); + + const createdUserAction = builder.build(); + render( + <TestProviders> + <EuiCommentList comments={createdUserAction} /> + </TestProviders> + ); + + expect(screen.getByText('themselves')).toBeInTheDocument(); + expect(screen.queryByText('Physical Dinosaur')).not.toBeInTheDocument(); + expect(screen.queryByText('and')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/assignees.tsx b/x-pack/plugins/cases/public/components/user_actions/assignees.tsx index 42580ede0b3f27..e0a499df056336 100644 --- a/x-pack/plugins/cases/public/components/user_actions/assignees.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/assignees.tsx @@ -5,13 +5,165 @@ * 2.0. */ -import type { UserActionBuilder } from './types'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { UserProfileWithAvatar } from '@kbn/user-profile-components'; +import React, { memo } from 'react'; +import { SnakeToCamelCase } from '../../../common/types'; +import { Actions, AssigneesUserAction, User } from '../../../common/api'; +import { getName } from '../user_profiles/display_name'; +import { Assignee } from '../user_profiles/types'; +import { UserToolTip } from '../user_profiles/user_tooltip'; +import { createCommonUpdateUserActionBuilder } from './common'; +import type { UserActionBuilder, UserActionResponse } from './types'; +import * as i18n from './translations'; +import { getUsernameDataTestSubj } from '../user_profiles/data_test_subject'; + +const FormatListItem: React.FC<{ + children: React.ReactElement; + index: number; + listSize: number; +}> = ({ children, index, listSize }) => { + if (shouldAddAnd(index, listSize)) { + return ( + <> + {i18n.AND} {children} + </> + ); + } else if (shouldAddComma(index, listSize)) { + return ( + <> + {children} + {','} + </> + ); + } + + return children; +}; +FormatListItem.displayName = 'FormatListItem'; + +export const shouldAddComma = (index: number, arrayLength: number) => { + return arrayLength > 2 && index !== arrayLength - 1; +}; + +export const shouldAddAnd = (index: number, arrayLength: number) => { + return arrayLength > 1 && index === arrayLength - 1; +}; + +const Themselves: React.FC<{ + index: number; + numOfAssigness: number; +}> = ({ index, numOfAssigness }) => ( + <FormatListItem index={index} listSize={numOfAssigness}> + <>{i18n.THEMSELVES}</> + </FormatListItem> +); +Themselves.displayName = 'Themselves'; + +const AssigneeComponent: React.FC<{ + assignee: Assignee; + index: number; + numOfAssigness: number; +}> = ({ assignee, index, numOfAssigness }) => ( + <FormatListItem index={index} listSize={numOfAssigness}> + <UserToolTip profile={assignee.profile}> + <strong>{getName(assignee.profile?.user)}</strong> + </UserToolTip> + </FormatListItem> +); +AssigneeComponent.displayName = 'Assignee'; + +interface AssigneesProps { + assignees: Assignee[]; + createdByUser: SnakeToCamelCase<User>; +} + +const AssigneesComponent = ({ assignees, createdByUser }: AssigneesProps) => ( + <> + {assignees.length > 0 && ( + <EuiFlexGroup alignItems="center" gutterSize="xs" wrap> + {assignees.map((assignee, index) => { + const usernameDataTestSubj = getUsernameDataTestSubj(assignee); + + return ( + <EuiFlexItem + data-test-subj={`ua-assignee-${usernameDataTestSubj}`} + grow={false} + key={assignee.uid} + > + <EuiText size="s" className="eui-textBreakWord"> + {doesAssigneeMatchCreatedByUser(assignee, createdByUser) ? ( + <Themselves index={index} numOfAssigness={assignees.length} /> + ) : ( + <AssigneeComponent + assignee={assignee} + index={index} + numOfAssigness={assignees.length} + /> + )} + </EuiText> + </EuiFlexItem> + ); + })} + </EuiFlexGroup> + )} + </> +); +AssigneesComponent.displayName = 'Assignees'; +const Assignees = memo(AssigneesComponent); + +const doesAssigneeMatchCreatedByUser = ( + assignee: Assignee, + createdByUser: SnakeToCamelCase<User> +) => { + return ( + assignee.uid === createdByUser?.profileUid || + // cases created before the assignees functionality will not have the profileUid so we'll need to fallback to the + // next best field + assignee?.profile?.user.username === createdByUser.username + ); +}; + +const getLabelTitle = ( + userAction: UserActionResponse<AssigneesUserAction>, + userProfiles?: Map<string, UserProfileWithAvatar> +) => { + const assignees = userAction.payload.assignees.map((assignee) => { + const profile = userProfiles?.get(assignee.uid); + return { + uid: assignee.uid, + profile, + }; + }); + + return ( + <EuiFlexGroup alignItems="baseline" gutterSize="xs" component="span" responsive={false}> + <EuiFlexItem data-test-subj="ua-assignees-label" grow={false}> + {userAction.action === Actions.add && i18n.ASSIGNED} + {userAction.action === Actions.delete && i18n.UNASSIGNED} + </EuiFlexItem> + <EuiFlexItem grow={false}> + <Assignees createdByUser={userAction.createdBy} assignees={assignees} /> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; export const createAssigneesUserActionBuilder: UserActionBuilder = ({ userAction, handleOutlineComment, + userProfiles, }) => ({ build: () => { - return []; + const assigneesUserAction = userAction as UserActionResponse<AssigneesUserAction>; + const label = getLabelTitle(assigneesUserAction, userProfiles); + const commonBuilder = createCommonUpdateUserActionBuilder({ + userAction, + handleOutlineComment, + label, + icon: 'userAvatar', + }); + + return commonBuilder.build(); }, }); diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/comment.test.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/comment.test.tsx index 66dbf74e6815b7..f8dba47578d72b 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/comment.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/comment.test.tsx @@ -8,6 +8,8 @@ import React from 'react'; import { EuiCommentList } from '@elastic/eui'; import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; import { Actions } from '../../../../common/api'; import { @@ -263,6 +265,47 @@ describe('createCommentUserActionBuilder', () => { expect(result.getByTestId('comment-externalReference-.test')).toBeInTheDocument(); expect(screen.getByText('Attachment actions')).toBeInTheDocument(); }); + + it('deletes the attachment correctly', async () => { + const externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry(); + externalReferenceAttachmentTypeRegistry.register(getExternalReferenceAttachment()); + + const userAction = getExternalReferenceUserAction(); + const builder = createCommentUserActionBuilder({ + ...builderArgs, + externalReferenceAttachmentTypeRegistry, + caseData: { + ...builderArgs.caseData, + comments: [externalReferenceAttachment], + }, + userAction, + }); + + const createdUserAction = builder.build(); + const result = appMockRender.render(<EuiCommentList comments={createdUserAction} />); + + expect(result.getByTestId('comment-externalReference-.test')).toBeInTheDocument(); + expect(result.getByTestId('property-actions')).toBeInTheDocument(); + + userEvent.click(result.getByTestId('property-actions-ellipses')); + await waitForEuiPopoverOpen(); + + expect(result.queryByTestId('property-actions-trash')).toBeInTheDocument(); + + userEvent.click(result.getByTestId('property-actions-trash')); + + await waitFor(() => { + expect(result.queryByTestId('property-actions-confirm-modal')).toBeInTheDocument(); + }); + + userEvent.click(result.getByText('Delete')); + + await waitFor(() => { + expect(builderArgs.handleDeleteComment).toHaveBeenCalledWith( + 'external-reference-comment-id' + ); + }); + }); }); describe('Persistable state', () => { @@ -398,5 +441,47 @@ describe('createCommentUserActionBuilder', () => { expect(result.getByTestId('comment-persistableState-.test')).toBeInTheDocument(); expect(screen.getByText('Attachment actions')).toBeInTheDocument(); }); + + it('deletes the attachment correctly', async () => { + const attachment = getPersistableStateAttachment(); + const persistableStateAttachmentTypeRegistry = new PersistableStateAttachmentTypeRegistry(); + persistableStateAttachmentTypeRegistry.register(attachment); + + const userAction = getPersistableStateUserAction(); + const builder = createCommentUserActionBuilder({ + ...builderArgs, + persistableStateAttachmentTypeRegistry, + caseData: { + ...builderArgs.caseData, + comments: [persistableStateAttachment], + }, + userAction, + }); + + const createdUserAction = builder.build(); + const result = appMockRender.render(<EuiCommentList comments={createdUserAction} />); + + expect(result.getByTestId('comment-persistableState-.test')).toBeInTheDocument(); + expect(result.getByTestId('property-actions')).toBeInTheDocument(); + + userEvent.click(result.getByTestId('property-actions-ellipses')); + await waitForEuiPopoverOpen(); + + expect(result.queryByTestId('property-actions-trash')).toBeInTheDocument(); + + userEvent.click(result.getByTestId('property-actions-trash')); + + await waitFor(() => { + expect(result.queryByTestId('property-actions-confirm-modal')).toBeInTheDocument(); + }); + + userEvent.click(result.getByText('Delete')); + + await waitFor(() => { + expect(builderArgs.handleDeleteComment).toHaveBeenCalledWith( + 'persistable-state-comment-id' + ); + }); + }); }); }); diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/comment.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/comment.tsx index ef5b4418d454f7..e04738a962686e 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/comment.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/comment.tsx @@ -63,7 +63,12 @@ const getCreateCommentUserAction = ({ comment: Comment; } & Omit< UserActionBuilderArgs, - 'caseServices' | 'comments' | 'index' | 'handleOutlineComment' + | 'caseServices' + | 'comments' + | 'index' + | 'handleOutlineComment' + | 'userProfiles' + | 'currentUserProfile' >): EuiCommentProps[] => { switch (comment.type) { case CommentType.user: @@ -109,6 +114,8 @@ const getCreateCommentUserAction = ({ comment, externalReferenceAttachmentTypeRegistry, caseData, + isLoading: loadingCommentIds.includes(comment.id), + handleDeleteComment, }); return externalReferenceBuilder.build(); @@ -119,6 +126,8 @@ const getCreateCommentUserAction = ({ comment, persistableStateAttachmentTypeRegistry, caseData, + isLoading: loadingCommentIds.includes(comment.id), + handleDeleteComment, }); return persistableBuilder.build(); diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/external_reference.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/external_reference.tsx index 10b676be711b71..f2b5987d8cb1df 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/external_reference.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/external_reference.tsx @@ -12,9 +12,10 @@ import { createRegisteredAttachmentUserActionBuilder } from './registered_attach type BuilderArgs = Pick< UserActionBuilderArgs, - 'userAction' | 'externalReferenceAttachmentTypeRegistry' | 'caseData' + 'userAction' | 'externalReferenceAttachmentTypeRegistry' | 'caseData' | 'handleDeleteComment' > & { comment: SnakeToCamelCase<CommentResponseExternalReferenceType>; + isLoading: boolean; }; export const createExternalReferenceAttachmentUserActionBuilder = ({ @@ -22,12 +23,16 @@ export const createExternalReferenceAttachmentUserActionBuilder = ({ comment, externalReferenceAttachmentTypeRegistry, caseData, + isLoading, + handleDeleteComment, }: BuilderArgs): ReturnType<UserActionBuilder> => { return createRegisteredAttachmentUserActionBuilder({ userAction, comment, registry: externalReferenceAttachmentTypeRegistry, caseData, + handleDeleteComment, + isLoading, getId: () => comment.externalReferenceAttachmentTypeId, getAttachmentViewProps: () => ({ externalReferenceId: comment.externalReferenceId, diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/persistable_state.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/persistable_state.tsx index 80e4d0d3743ce0..2d5cfa91e1500a 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/persistable_state.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/persistable_state.tsx @@ -12,9 +12,10 @@ import { createRegisteredAttachmentUserActionBuilder } from './registered_attach type BuilderArgs = Pick< UserActionBuilderArgs, - 'userAction' | 'persistableStateAttachmentTypeRegistry' | 'caseData' + 'userAction' | 'persistableStateAttachmentTypeRegistry' | 'caseData' | 'handleDeleteComment' > & { comment: SnakeToCamelCase<CommentResponseTypePersistableState>; + isLoading: boolean; }; export const createPersistableStateAttachmentUserActionBuilder = ({ @@ -22,12 +23,16 @@ export const createPersistableStateAttachmentUserActionBuilder = ({ comment, persistableStateAttachmentTypeRegistry, caseData, + isLoading, + handleDeleteComment, }: BuilderArgs): ReturnType<UserActionBuilder> => { return createRegisteredAttachmentUserActionBuilder({ userAction, comment, registry: persistableStateAttachmentTypeRegistry, caseData, + handleDeleteComment, + isLoading, getId: () => comment.persistableStateAttachmentTypeId, getAttachmentViewProps: () => ({ persistableStateAttachmentTypeId: comment.persistableStateAttachmentTypeId, diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/registered_attachments.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/registered_attachments.tsx index 6d07d9111fc8a2..446cb6114e97e8 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/registered_attachments.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/registered_attachments.tsx @@ -22,12 +22,17 @@ import { UserActionBuilder, UserActionBuilderArgs } from '../types'; import { UserActionTimestamp } from '../timestamp'; import { SnakeToCamelCase } from '../../../../common/types'; import { UserActionUsernameWithAvatar } from '../avatar_username'; -import { UserActionCopyLink } from '../copy_link'; import { ATTACHMENT_NOT_REGISTERED_ERROR, DEFAULT_EVENT_ATTACHMENT_TITLE } from './translations'; +import { UserActionContentToolbar } from '../content_toolbar'; +import * as i18n from '../translations'; -type BuilderArgs<C, R> = Pick<UserActionBuilderArgs, 'userAction' | 'caseData'> & { +type BuilderArgs<C, R> = Pick< + UserActionBuilderArgs, + 'userAction' | 'caseData' | 'handleDeleteComment' +> & { comment: SnakeToCamelCase<C>; registry: R; + isLoading: boolean; getId: () => string; getAttachmentViewProps: () => object; }; @@ -64,8 +69,10 @@ export const createRegisteredAttachmentUserActionBuilder = < comment, registry, caseData, + isLoading, getId, getAttachmentViewProps, + handleDeleteComment, }: BuilderArgs<C, R>): ReturnType<UserActionBuilder> => ({ // TODO: Fix this manually. Issue #123375 // eslint-disable-next-line react/display-name @@ -122,8 +129,15 @@ export const createRegisteredAttachmentUserActionBuilder = < timelineAvatar: attachmentViewObject.timelineAvatar, actions: ( <> - <UserActionCopyLink id={comment.id} /> - {attachmentViewObject.actions} + <UserActionContentToolbar + actions={['delete']} + id={comment.id} + deleteLabel={i18n.DELETE_COMMENT} + deleteConfirmTitle={i18n.DELETE_COMMENT_TITLE} + isLoading={isLoading} + onDelete={() => handleDeleteComment(comment.id)} + extraActions={attachmentViewObject.actions} + /> </> ), children: renderer(props), diff --git a/x-pack/plugins/cases/public/components/user_actions/content_toolbar.tsx b/x-pack/plugins/cases/public/components/user_actions/content_toolbar.tsx index e23a4efa2f0a21..b824f6f20276f0 100644 --- a/x-pack/plugins/cases/public/components/user_actions/content_toolbar.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/content_toolbar.tsx @@ -6,32 +6,36 @@ */ import React, { memo } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiCommentProps, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { UserActionCopyLink } from './copy_link'; -import { UserActionPropertyActions } from './property_actions'; +import { Actions, UserActionPropertyActions } from './property_actions'; export interface UserActionContentToolbarProps { - commentMarkdown: string; + commentMarkdown?: string; id: string; - editLabel: string; + actions?: Actions; + editLabel?: string; deleteLabel?: string; deleteConfirmTitle?: string; - quoteLabel: string; + quoteLabel?: string; isLoading: boolean; - onEdit: (id: string) => void; - onQuote: (id: string) => void; + extraActions?: EuiCommentProps['actions']; + onEdit?: (id: string) => void; + onQuote?: (id: string) => void; onDelete?: (id: string) => void; } const UserActionContentToolbarComponent = ({ commentMarkdown, id, + actions, editLabel, deleteLabel, deleteConfirmTitle, quoteLabel, isLoading, + extraActions, onEdit, onQuote, onDelete, @@ -43,6 +47,7 @@ const UserActionContentToolbarComponent = ({ <EuiFlexItem grow={false}> <UserActionPropertyActions id={id} + actions={actions} editLabel={editLabel} quoteLabel={quoteLabel} deleteLabel={deleteLabel} @@ -54,6 +59,7 @@ const UserActionContentToolbarComponent = ({ commentMarkdown={commentMarkdown} /> </EuiFlexItem> + {extraActions != null ? <EuiFlexItem grow={false}>{extraActions}</EuiFlexItem> : null} </EuiFlexGroup> ); UserActionContentToolbarComponent.displayName = 'UserActionContentToolbar'; diff --git a/x-pack/plugins/cases/public/components/user_actions/index.test.tsx b/x-pack/plugins/cases/public/components/user_actions/index.test.tsx index 60fc0e92d024bf..0991156cd3d4dc 100644 --- a/x-pack/plugins/cases/public/components/user_actions/index.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/index.test.tsx @@ -32,6 +32,8 @@ const onShowAlertDetails = jest.fn(); const defaultProps = { caseServices: {}, caseUserActions: [], + userProfiles: new Map(), + currentUserProfile: undefined, connectors: [], actionsNavigation: { href: jest.fn(), onClick: jest.fn() }, getRuleDetailsHref: jest.fn(), @@ -440,6 +442,7 @@ describe(`UserActions`, () => { ).toBe('lock'); }); }); + it('shows a lockOpen icon if the action is unisolate/release', async () => { const isolateAction = [getHostIsolationUserAction()]; const props = { diff --git a/x-pack/plugins/cases/public/components/user_actions/index.tsx b/x-pack/plugins/cases/public/components/user_actions/index.tsx index 4a6bc85c7cbd70..1517450c4f62eb 100644 --- a/x-pack/plugins/cases/public/components/user_actions/index.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/index.tsx @@ -81,6 +81,8 @@ export const UserActions = React.memo( ({ caseServices, caseUserActions, + userProfiles, + currentUserProfile, data: caseData, getRuleDetailsHref, actionsNavigation, @@ -183,6 +185,8 @@ export const UserActions = React.memo( externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, userAction, + userProfiles, + currentUserProfile, caseServices, comments: caseData.comments, index, @@ -208,6 +212,8 @@ export const UserActions = React.memo( ), [ caseUserActions, + userProfiles, + currentUserProfile, externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, descriptionCommentListObj, diff --git a/x-pack/plugins/cases/public/components/user_actions/mock.ts b/x-pack/plugins/cases/public/components/user_actions/mock.ts index b3a7909b06929d..b963947a6282d5 100644 --- a/x-pack/plugins/cases/public/components/user_actions/mock.ts +++ b/x-pack/plugins/cases/public/components/user_actions/mock.ts @@ -10,6 +10,7 @@ import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry'; import { PersistableStateAttachmentTypeRegistry } from '../../client/attachment_framework/persistable_state_registry'; import { basicCase, basicPush, getUserAction } from '../../containers/mock'; +import { userProfiles, userProfilesMap } from '../../containers/user_profiles/api.mock'; import { UserActionBuilderArgs } from './types'; export const getMockBuilderArgs = (): UserActionBuilderArgs => { @@ -63,6 +64,8 @@ export const getMockBuilderArgs = (): UserActionBuilderArgs => { return { userAction, + userProfiles: userProfilesMap, + currentUserProfile: userProfiles[0], externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, caseData: basicCase, diff --git a/x-pack/plugins/cases/public/components/user_actions/property_actions.test.tsx b/x-pack/plugins/cases/public/components/user_actions/property_actions.test.tsx index 2f52656c9fca96..01a4605f1651f4 100644 --- a/x-pack/plugins/cases/public/components/user_actions/property_actions.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/property_actions.test.tsx @@ -213,4 +213,38 @@ describe('UserActionPropertyActions ', () => { expect(onDelete).toHaveBeenCalledWith(deleteProps.id); }); }); + + describe('action filtering', () => { + const tests = [ + ['edit', 'pencil'], + ['delete', 'trash'], + ['quote', 'quote'], + ] as const; + + it.each(tests)('renders action %s', async (action, type) => { + const renderResult = render( + <TestProviders> + <UserActionPropertyActions + {...props} + onDelete={() => {}} + deleteLabel={'test'} + actions={[action]} + /> + </TestProviders> + ); + + expect(renderResult.queryByTestId('user-action-title-loading')).not.toBeInTheDocument(); + expect(renderResult.getByTestId('property-actions')).toBeInTheDocument(); + + userEvent.click(renderResult.getByTestId('property-actions-ellipses')); + await waitForEuiPopoverOpen(); + + expect(renderResult.queryByTestId(`property-actions-${type}`)).toBeInTheDocument(); + /** + * This check ensures that no other action is rendered. There is + * one button to open the popover and one button for the action + **/ + expect(await renderResult.findAllByRole('button')).toHaveLength(2); + }); + }); }); diff --git a/x-pack/plugins/cases/public/components/user_actions/property_actions.tsx b/x-pack/plugins/cases/public/components/user_actions/property_actions.tsx index 13273346241e53..132b824109e519 100644 --- a/x-pack/plugins/cases/public/components/user_actions/property_actions.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/property_actions.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { noop } from 'lodash'; import React, { memo, useMemo, useCallback, useState } from 'react'; import { EuiConfirmModal, EuiLoadingSpinner } from '@elastic/eui'; @@ -13,33 +14,48 @@ import { useLensOpenVisualization } from '../markdown_editor/plugins/lens/use_le import { CANCEL_BUTTON, CONFIRM_BUTTON } from './translations'; import { useCasesContext } from '../cases_context/use_cases_context'; +const totalActions = { + edit: 'edit', + delete: 'delete', + quote: 'quote', + showLensEditor: 'showLensEditor', +} as const; + +const availableActions = Object.keys(totalActions) as Array<keyof typeof totalActions>; + +export type Actions = typeof availableActions; + export interface UserActionPropertyActionsProps { id: string; - editLabel: string; + actions?: Actions; + editLabel?: string; deleteLabel?: string; deleteConfirmTitle?: string; - quoteLabel: string; + quoteLabel?: string; isLoading: boolean; - onEdit: (id: string) => void; + onEdit?: (id: string) => void; onDelete?: (id: string) => void; - onQuote: (id: string) => void; - commentMarkdown: string; + onQuote?: (id: string) => void; + commentMarkdown?: string; } const UserActionPropertyActionsComponent = ({ id, - editLabel, - quoteLabel, - deleteLabel, + actions = availableActions, + editLabel = '', + quoteLabel = '', + deleteLabel = '', deleteConfirmTitle, isLoading, - onEdit, + onEdit = noop, onDelete, - onQuote, + onQuote = noop, commentMarkdown, }: UserActionPropertyActionsProps) => { const { permissions } = useCasesContext(); - const { canUseEditor, actionConfig } = useLensOpenVisualization({ comment: commentMarkdown }); + const { canUseEditor, actionConfig } = useLensOpenVisualization({ + comment: commentMarkdown ?? '', + }); const onEditClick = useCallback(() => onEdit(id), [id, onEdit]); const onQuoteClick = useCallback(() => onQuote(id), [id, onQuote]); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); @@ -60,10 +76,19 @@ const UserActionPropertyActionsComponent = ({ }, []); const propertyActions = useMemo(() => { - const showEditPencilIcon = permissions.update; - const showTrashIcon = permissions.delete && deleteLabel && onDelete; - const showQuoteIcon = permissions.create; - const showLensEditor = permissions.update && canUseEditor && actionConfig; + const showEditPencilIcon = permissions.update && actions.includes(totalActions.edit); + + const showTrashIcon = Boolean( + permissions.delete && deleteLabel && onDelete && actions.includes(totalActions.delete) + ); + + const showQuoteIcon = permissions.create && actions.includes(totalActions.quote); + + const showLensEditor = + permissions.update && + canUseEditor && + actionConfig && + actions.includes(totalActions.showLensEditor); return [ ...(showEditPencilIcon @@ -99,6 +124,7 @@ const UserActionPropertyActionsComponent = ({ permissions.update, permissions.delete, permissions.create, + actions, deleteLabel, onDelete, canUseEditor, diff --git a/x-pack/plugins/cases/public/components/user_actions/tags.tsx b/x-pack/plugins/cases/public/components/user_actions/tags.tsx index d5553a3f6f13d4..f9d0203a5647f4 100644 --- a/x-pack/plugins/cases/public/components/user_actions/tags.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/tags.tsx @@ -11,7 +11,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Actions, TagsUserAction } from '../../../common/api'; import { UserActionBuilder, UserActionResponse } from './types'; import { createCommonUpdateUserActionBuilder } from './common'; -import { Tags } from '../tag_list/tags'; +import { Tags } from '../tags/tags'; import * as i18n from './translations'; const getLabelTitle = (userAction: UserActionResponse<TagsUserAction>) => { diff --git a/x-pack/plugins/cases/public/components/user_actions/translations.ts b/x-pack/plugins/cases/public/components/user_actions/translations.ts index b5b5d902d3a4de..91425c368286d9 100644 --- a/x-pack/plugins/cases/public/components/user_actions/translations.ts +++ b/x-pack/plugins/cases/public/components/user_actions/translations.ts @@ -78,3 +78,19 @@ export const CANCEL_BUTTON = i18n.translate('xpack.cases.caseView.delete.cancel' export const CONFIRM_BUTTON = i18n.translate('xpack.cases.caseView.delete.confirm', { defaultMessage: 'Delete', }); + +export const ASSIGNED = i18n.translate('xpack.cases.caseView.assigned', { + defaultMessage: 'assigned', +}); + +export const UNASSIGNED = i18n.translate('xpack.cases.caseView.unAssigned', { + defaultMessage: 'unassigned', +}); + +export const THEMSELVES = i18n.translate('xpack.cases.caseView.assignee.themselves', { + defaultMessage: 'themselves', +}); + +export const AND = i18n.translate('xpack.cases.caseView.assignee.and', { + defaultMessage: 'and', +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/types.ts b/x-pack/plugins/cases/public/components/user_actions/types.ts index 8ba409468851ed..7477e6df8d5dce 100644 --- a/x-pack/plugins/cases/public/components/user_actions/types.ts +++ b/x-pack/plugins/cases/public/components/user_actions/types.ts @@ -6,6 +6,7 @@ */ import { EuiCommentProps } from '@elastic/eui'; +import { UserProfileWithAvatar } from '@kbn/user-profile-components'; import { SnakeToCamelCase } from '../../../common/types'; import { ActionTypes, UserActionWithResponse } from '../../../common/api'; import { Case, CaseUserActions, Comment, UseFetchAlertData } from '../../containers/types'; @@ -17,10 +18,13 @@ import { UNSUPPORTED_ACTION_TYPES } from './constants'; import type { OnUpdateFields } from '../case_view/types'; import { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry'; import { PersistableStateAttachmentTypeRegistry } from '../../client/attachment_framework/persistable_state_registry'; +import { CurrentUserProfile } from '../types'; export interface UserActionTreeProps { caseServices: CaseServices; caseUserActions: CaseUserActions[]; + userProfiles: Map<string, UserProfileWithAvatar>; + currentUserProfile: CurrentUserProfile; data: Case; getRuleDetailsHref?: RuleDetailsNavigation['href']; actionsNavigation?: ActionsNavigation; @@ -38,6 +42,8 @@ export type SupportedUserActionTypes = keyof Omit<typeof ActionTypes, Unsupporte export interface UserActionBuilderArgs { caseData: Case; + userProfiles: Map<string, UserProfileWithAvatar>; + currentUserProfile: CurrentUserProfile; externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry; persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry; userAction: CaseUserActions; diff --git a/x-pack/plugins/cases/public/components/tag_list/schema.tsx b/x-pack/plugins/cases/public/components/user_profiles/data_test_subject.ts similarity index 61% rename from x-pack/plugins/cases/public/components/tag_list/schema.tsx rename to x-pack/plugins/cases/public/components/user_profiles/data_test_subject.ts index d7db17bd97cbde..23d952738aa4dc 100644 --- a/x-pack/plugins/cases/public/components/tag_list/schema.tsx +++ b/x-pack/plugins/cases/public/components/user_profiles/data_test_subject.ts @@ -5,9 +5,8 @@ * 2.0. */ -import { FormSchema } from '../../common/shared_imports'; -import { schemaTags } from '../create/schema'; +import { Assignee } from './types'; -export const schema: FormSchema = { - tags: schemaTags, +export const getUsernameDataTestSubj = (assignee: Assignee) => { + return assignee.profile?.user.username ?? assignee.uid; }; diff --git a/x-pack/plugins/cases/public/components/user_profiles/display_name.test.ts b/x-pack/plugins/cases/public/components/user_profiles/display_name.test.ts new file mode 100644 index 00000000000000..fec173ac70c618 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_profiles/display_name.test.ts @@ -0,0 +1,38 @@ +/* + * 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 { getName } from './display_name'; + +describe('getName', () => { + it('returns unknown when the user is undefined', () => { + expect(getName()).toBe('Unknown'); + }); + + it('returns the full name', () => { + expect(getName({ full_name: 'name', username: 'username' })).toBe('name'); + }); + + it('returns the email if the full name is empty', () => { + expect(getName({ full_name: '', email: 'email', username: 'username' })).toBe('email'); + }); + + it('returns the email if the full name is undefined', () => { + expect(getName({ email: 'email', username: 'username' })).toBe('email'); + }); + + it('returns the username if the full name and email are empty', () => { + expect(getName({ full_name: '', email: '', username: 'username' })).toBe('username'); + }); + + it('returns the username if the full name and email are undefined', () => { + expect(getName({ username: 'username' })).toBe('username'); + }); + + it('returns the username is empty', () => { + expect(getName({ username: '' })).toBe('Unknown'); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_profiles/display_name.ts b/x-pack/plugins/cases/public/components/user_profiles/display_name.ts new file mode 100644 index 00000000000000..4abd9f276abaaf --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_profiles/display_name.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 { getUserDisplayName, UserProfileUserInfo } from '@kbn/user-profile-components'; +import { isEmpty } from 'lodash'; +import * as i18n from './translations'; + +export const getName = (user?: UserProfileUserInfo): string => { + if (!user) { + return i18n.UNKNOWN; + } + + const displayName = getUserDisplayName(user); + return !isEmpty(displayName) ? displayName : i18n.UNKNOWN; +}; diff --git a/x-pack/plugins/cases/public/components/user_profiles/empty_message.test.tsx b/x-pack/plugins/cases/public/components/user_profiles/empty_message.test.tsx new file mode 100644 index 00000000000000..3c0c935d9316e8 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_profiles/empty_message.test.tsx @@ -0,0 +1,17 @@ +/* + * 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 from 'react'; +import { EmptyMessage } from './empty_message'; +import { render } from '@testing-library/react'; + +describe('EmptyMessage', () => { + it('renders a null component', () => { + const { container } = render(<EmptyMessage />); + expect(container.firstChild).toBeNull(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_profiles/empty_message.tsx b/x-pack/plugins/cases/public/components/user_profiles/empty_message.tsx new file mode 100644 index 00000000000000..a2c713d5a9abce --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_profiles/empty_message.tsx @@ -0,0 +1,13 @@ +/* + * 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 from 'react'; + +const EmptyMessageComponent: React.FC = () => null; +EmptyMessageComponent.displayName = 'EmptyMessage'; + +export const EmptyMessage = React.memo(EmptyMessageComponent); diff --git a/x-pack/plugins/cases/public/components/user_profiles/no_matches.test.tsx b/x-pack/plugins/cases/public/components/user_profiles/no_matches.test.tsx new file mode 100644 index 00000000000000..3471aad3fec3c3 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_profiles/no_matches.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { NoMatches } from './no_matches'; +import { render, screen } from '@testing-library/react'; + +describe('NoMatches', () => { + it('renders the no matches messages', () => { + render(<NoMatches />); + + expect(screen.getByText('No matching users with required access.')); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_profiles/no_matches.tsx b/x-pack/plugins/cases/public/components/user_profiles/no_matches.tsx new file mode 100644 index 00000000000000..638d705fade867 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_profiles/no_matches.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer, EuiText, EuiTextAlign } from '@elastic/eui'; +import React from 'react'; +import * as i18n from './translations'; + +const NoMatchesComponent: React.FC = () => { + return ( + <EuiFlexGroup + alignItems="center" + gutterSize="none" + direction="column" + justifyContent="spaceAround" + data-test-subj="case-user-profiles-assignees-popover-no-matches" + > + <EuiFlexItem grow={false}> + <EuiIcon type="userAvatar" size="xl" /> + <EuiSpacer size="xs" /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiTextAlign textAlign="center"> + <EuiText size="s" color="default"> + <strong>{i18n.NO_MATCHING_USERS}</strong> + <br /> + </EuiText> + <EuiText size="s" color="subdued"> + {i18n.TRY_MODIFYING_SEARCH} + </EuiText> + </EuiTextAlign> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; +NoMatchesComponent.displayName = 'NoMatches'; + +export const NoMatches = React.memo(NoMatchesComponent); diff --git a/x-pack/plugins/cases/public/components/user_profiles/selected_status_message.test.tsx b/x-pack/plugins/cases/public/components/user_profiles/selected_status_message.test.tsx new file mode 100644 index 00000000000000..b9611bb683d447 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_profiles/selected_status_message.test.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { SelectedStatusMessage } from './selected_status_message'; + +describe('SelectedStatusMessage', () => { + it('does not render if the count is 0', () => { + const { container } = render(<SelectedStatusMessage selectedCount={0} message={'hello'} />); + + expect(container.firstChild).toBeNull(); + expect(screen.queryByText('hello')).not.toBeInTheDocument(); + }); + + it('renders the message when the count is great than 0', () => { + render(<SelectedStatusMessage selectedCount={1} message={'hello'} />); + + expect(screen.getByText('hello')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_profiles/selected_status_message.tsx b/x-pack/plugins/cases/public/components/user_profiles/selected_status_message.tsx new file mode 100644 index 00000000000000..87839fb7c34826 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_profiles/selected_status_message.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +const SelectedStatusMessageComponent: React.FC<{ + selectedCount: number; + message: string; +}> = ({ selectedCount, message }) => { + if (selectedCount <= 0) { + return null; + } + + return <>{message}</>; +}; +SelectedStatusMessageComponent.displayName = 'SelectedStatusMessage'; + +export const SelectedStatusMessage = React.memo(SelectedStatusMessageComponent); diff --git a/x-pack/plugins/cases/public/components/user_profiles/sort.test.ts b/x-pack/plugins/cases/public/components/user_profiles/sort.test.ts new file mode 100644 index 00000000000000..d2f64a05e7ce1c --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_profiles/sort.test.ts @@ -0,0 +1,118 @@ +/* + * 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 { userProfiles } from '../../containers/user_profiles/api.mock'; +import { bringCurrentUserToFrontAndSort, moveCurrentUserToBeginning } from './sort'; + +describe('sort', () => { + describe('moveCurrentUserToBeginning', () => { + it('returns an empty array if no profiles are provided', () => { + expect(moveCurrentUserToBeginning()).toBeUndefined(); + }); + + it("returns the profiles if the current profile isn't provided", () => { + const profiles = [{ uid: '1' }]; + expect(moveCurrentUserToBeginning(undefined, profiles)).toEqual(profiles); + }); + + it("returns the profiles if the current profile isn't found", () => { + const profiles = [{ uid: '1' }]; + expect(moveCurrentUserToBeginning({ uid: '2' }, profiles)).toEqual(profiles); + }); + + it('moves the current profile to the front', () => { + const profiles = [{ uid: '1' }, { uid: '2' }]; + expect(moveCurrentUserToBeginning({ uid: '2' }, profiles)).toEqual([ + { uid: '2' }, + { uid: '1' }, + ]); + }); + }); + + describe('bringCurrentUserToFrontAndSort', () => { + const unsortedProfiles = [...userProfiles].reverse(); + + it('returns a sorted list of users when the current user is undefined', () => { + expect(bringCurrentUserToFrontAndSort(undefined, unsortedProfiles)).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object {}, + "enabled": true, + "uid": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0", + "user": Object { + "email": "damaged_raccoon@elastic.co", + "full_name": "Damaged Raccoon", + "username": "damaged_raccoon", + }, + }, + Object { + "data": Object {}, + "enabled": true, + "uid": "u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0", + "user": Object { + "email": "physical_dinosaur@elastic.co", + "full_name": "Physical Dinosaur", + "username": "physical_dinosaur", + }, + }, + Object { + "data": Object {}, + "enabled": true, + "uid": "u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0", + "user": Object { + "email": "wet_dingo@elastic.co", + "full_name": "Wet Dingo", + "username": "wet_dingo", + }, + }, + ] + `); + }); + + it('returns a sorted list of users with the current user at the beginning', () => { + expect(bringCurrentUserToFrontAndSort(userProfiles[2], unsortedProfiles)) + .toMatchInlineSnapshot(` + Array [ + Object { + "data": Object {}, + "enabled": true, + "uid": "u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0", + "user": Object { + "email": "wet_dingo@elastic.co", + "full_name": "Wet Dingo", + "username": "wet_dingo", + }, + }, + Object { + "data": Object {}, + "enabled": true, + "uid": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0", + "user": Object { + "email": "damaged_raccoon@elastic.co", + "full_name": "Damaged Raccoon", + "username": "damaged_raccoon", + }, + }, + Object { + "data": Object {}, + "enabled": true, + "uid": "u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0", + "user": Object { + "email": "physical_dinosaur@elastic.co", + "full_name": "Physical Dinosaur", + "username": "physical_dinosaur", + }, + }, + ] + `); + }); + + it('returns undefined if profiles is undefined', () => { + expect(bringCurrentUserToFrontAndSort(userProfiles[2], undefined)).toBeUndefined(); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_profiles/sort.ts b/x-pack/plugins/cases/public/components/user_profiles/sort.ts new file mode 100644 index 00000000000000..e1e8018a21e356 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_profiles/sort.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UserProfileWithAvatar } from '@kbn/user-profile-components'; +import { sortBy } from 'lodash'; +import { CurrentUserProfile } from '../types'; + +export const getSortField = (profile: UserProfileWithAvatar) => + profile.user.full_name?.toLowerCase() ?? + profile.user.email?.toLowerCase() ?? + profile.user.username.toLowerCase(); + +export const moveCurrentUserToBeginning = <T extends { uid: string }>( + currentUserProfile?: T, + profiles?: T[] +) => { + if (!profiles) { + return; + } + + if (!currentUserProfile) { + return profiles; + } + + const currentProfileIndex = profiles.find((profile) => profile.uid === currentUserProfile.uid); + + if (!currentProfileIndex) { + return profiles; + } + + const profilesWithoutCurrentUser = profiles.filter( + (profile) => profile.uid !== currentUserProfile.uid + ); + + return [currentUserProfile, ...profilesWithoutCurrentUser]; +}; + +export const bringCurrentUserToFrontAndSort = ( + currentUserProfile: CurrentUserProfile, + profiles?: UserProfileWithAvatar[] +) => moveCurrentUserToBeginning(currentUserProfile, sortProfiles(profiles)); + +export const sortProfiles = (profiles?: UserProfileWithAvatar[]) => { + if (!profiles) { + return; + } + + return sortBy(profiles, getSortField); +}; diff --git a/x-pack/plugins/cases/public/components/user_profiles/translations.ts b/x-pack/plugins/cases/public/components/user_profiles/translations.ts new file mode 100644 index 00000000000000..beded4faf714b7 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_profiles/translations.ts @@ -0,0 +1,52 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export * from '../../common/translations'; + +export const REMOVE_ASSIGNEE = i18n.translate('xpack.cases.userProfile.removeAssigneeToolTip', { + defaultMessage: 'Remove assignee', +}); + +export const REMOVE_ASSIGNEE_ARIA_LABEL = i18n.translate( + 'xpack.cases.userProfile.removeAssigneeAriaLabel', + { + defaultMessage: 'click to remove assignee', + } +); + +export const MISSING_PROFILE = i18n.translate('xpack.cases.userProfile.missingProfile', { + defaultMessage: 'Unable to find user profile', +}); + +export const SEARCH_USERS = i18n.translate('xpack.cases.userProfile.selectableSearchPlaceholder', { + defaultMessage: 'Search users', +}); + +export const EDIT_ASSIGNEES = i18n.translate('xpack.cases.userProfile.editAssignees', { + defaultMessage: 'Edit assignees', +}); + +export const REMOVE_ASSIGNEES = i18n.translate( + 'xpack.cases.userProfile.suggestUsers.removeAssignees', + { + defaultMessage: 'Remove all assignees', + } +); + +export const ASSIGNEES = i18n.translate('xpack.cases.userProfile.assigneesTitle', { + defaultMessage: 'Assignees', +}); + +export const NO_MATCHING_USERS = i18n.translate('xpack.cases.userProfiles.noMatchingUsers', { + defaultMessage: 'No matching users with required access.', +}); + +export const TRY_MODIFYING_SEARCH = i18n.translate('xpack.cases.userProfiles.tryModifyingSearch', { + defaultMessage: 'Try modifying your search.', +}); diff --git a/x-pack/test/visual_regression/ftr_provider_context.d.ts b/x-pack/plugins/cases/public/components/user_profiles/types.ts similarity index 51% rename from x-pack/test/visual_regression/ftr_provider_context.d.ts rename to x-pack/plugins/cases/public/components/user_profiles/types.ts index 24f5087ef7fe2f..f4acb29809d68b 100644 --- a/x-pack/test/visual_regression/ftr_provider_context.d.ts +++ b/x-pack/plugins/cases/public/components/user_profiles/types.ts @@ -5,9 +5,13 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test'; +import { UserProfileWithAvatar } from '@kbn/user-profile-components'; -import { pageObjects } from './page_objects'; -import { services } from './services'; +export interface Assignee { + uid: string; + profile?: UserProfileWithAvatar; +} -export type FtrProviderContext = GenericFtrProviderContext<typeof services, typeof pageObjects>; +export interface AssigneeWithProfile extends Assignee { + profile: UserProfileWithAvatar; +} diff --git a/x-pack/plugins/cases/public/components/user_profiles/unknown_user.tsx b/x-pack/plugins/cases/public/components/user_profiles/unknown_user.tsx new file mode 100644 index 00000000000000..b98eef9efbd9f5 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_profiles/unknown_user.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { UserAvatar, UserAvatarProps } from '@kbn/user-profile-components'; + +interface CaseUnknownUserAvatarProps { + size: UserAvatarProps['size']; +} + +const CaseUnknownUserAvatarComponent: React.FC<CaseUnknownUserAvatarProps> = ({ size }) => { + return <UserAvatar data-test-subj="case-user-profile-avatar-unknown-user" size={size} />; +}; +CaseUnknownUserAvatarComponent.displayName = 'UnknownUserAvatar'; + +export const CaseUnknownUserAvatar = React.memo(CaseUnknownUserAvatarComponent); diff --git a/x-pack/plugins/cases/public/components/user_profiles/user_avatar.test.tsx b/x-pack/plugins/cases/public/components/user_profiles/user_avatar.test.tsx new file mode 100644 index 00000000000000..1337239bf2dcc4 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_profiles/user_avatar.test.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen } from '@testing-library/react'; +import { AppMockRenderer, createAppMockRenderer } from '../../common/mock'; +import { userProfiles } from '../../containers/user_profiles/api.mock'; +import { CaseUserAvatar } from './user_avatar'; + +describe('CaseUserAvatar', () => { + let appMockRender: AppMockRenderer; + + beforeEach(() => { + appMockRender = createAppMockRenderer(); + }); + + it('renders the avatar of Damaged Raccoon profile', () => { + appMockRender.render(<CaseUserAvatar size="s" profile={userProfiles[0]} />); + + expect(screen.getByText('DR')).toBeInTheDocument(); + }); + + it('renders the avatar of the unknown profile', () => { + appMockRender.render(<CaseUserAvatar size="s" />); + + expect(screen.getByText('?')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_profiles/user_avatar.tsx b/x-pack/plugins/cases/public/components/user_profiles/user_avatar.tsx new file mode 100644 index 00000000000000..be6a8ddfc9359f --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_profiles/user_avatar.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { UserAvatar, UserProfileWithAvatar, UserAvatarProps } from '@kbn/user-profile-components'; +import { CaseUnknownUserAvatar } from './unknown_user'; + +interface CaseUserAvatarProps { + size: UserAvatarProps['size']; + profile?: UserProfileWithAvatar; +} + +const CaseUserAvatarComponent: React.FC<CaseUserAvatarProps> = ({ size, profile }) => { + const dataTestSubjName = profile?.user.username; + + return profile !== undefined ? ( + <UserAvatar + user={profile.user} + avatar={profile.data.avatar} + data-test-subj={`case-user-profile-avatar-${dataTestSubjName}`} + size={size} + /> + ) : ( + <CaseUnknownUserAvatar size={size} /> + ); +}; + +CaseUserAvatarComponent.displayName = 'CaseUserAvatar'; + +export const CaseUserAvatar = React.memo(CaseUserAvatarComponent); diff --git a/x-pack/plugins/cases/public/components/user_profiles/user_representation.test.tsx b/x-pack/plugins/cases/public/components/user_profiles/user_representation.test.tsx new file mode 100644 index 00000000000000..5bda7ef8d3cbab --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_profiles/user_representation.test.tsx @@ -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 React from 'react'; +import { fireEvent, screen } from '@testing-library/react'; +import { UserRepresentation, UserRepresentationProps } from './user_representation'; +import { userProfiles } from '../../containers/user_profiles/api.mock'; +import { + AppMockRenderer, + createAppMockRenderer, + noUpdateCasesPermissions, +} from '../../common/mock'; + +describe('UserRepresentation', () => { + const dataTestSubjGroup = `user-profile-assigned-user-group-${userProfiles[0].user.username}`; + const dataTestSubjCross = `user-profile-assigned-user-cross-${userProfiles[0].user.username}`; + const dataTestSubjGroupUnknown = `user-profile-assigned-user-group-unknownId`; + const dataTestSubjCrossUnknown = `user-profile-assigned-user-cross-unknownId`; + + let defaultProps: UserRepresentationProps; + let appMockRender: AppMockRenderer; + + beforeEach(() => { + defaultProps = { + assignee: { uid: userProfiles[0].uid, profile: userProfiles[0] }, + onRemoveAssignee: jest.fn(), + }; + + appMockRender = createAppMockRenderer(); + }); + + it('does not show the cross button when the user is not hovering over the row', () => { + appMockRender.render(<UserRepresentation {...defaultProps} />); + + expect(screen.queryByTestId(dataTestSubjCross)).toHaveStyle('opacity: 0'); + }); + + it('show the cross button when the user is hovering over the row', () => { + appMockRender.render(<UserRepresentation {...defaultProps} />); + + fireEvent.mouseEnter(screen.getByTestId(dataTestSubjGroup)); + + expect(screen.getByTestId(dataTestSubjCross)).toHaveStyle('opacity: 1'); + }); + + it('does not show the cross button when the user is hovering over the row and does not have update permissions', () => { + appMockRender = createAppMockRenderer({ permissions: noUpdateCasesPermissions() }); + appMockRender.render(<UserRepresentation {...defaultProps} />); + + fireEvent.mouseEnter(screen.getByTestId(dataTestSubjGroup)); + + expect(screen.queryByTestId(dataTestSubjCross)).not.toBeInTheDocument(); + }); + + it('show the cross button when hovering over the row of an unknown user', () => { + appMockRender.render( + <UserRepresentation {...{ ...defaultProps, assignee: { uid: 'unknownId' } }} /> + ); + + fireEvent.mouseEnter(screen.getByTestId(dataTestSubjGroupUnknown)); + + expect(screen.getByTestId(dataTestSubjCrossUnknown)).toHaveStyle('opacity: 1'); + }); + + it('shows and then removes the cross button when the user hovers and removes the mouse from over the row', () => { + appMockRender.render(<UserRepresentation {...defaultProps} />); + + fireEvent.mouseEnter(screen.getByTestId(dataTestSubjGroup)); + expect(screen.getByTestId(dataTestSubjCross)).toHaveStyle('opacity: 1'); + + fireEvent.mouseLeave(screen.getByTestId(dataTestSubjGroup)); + expect(screen.queryByTestId(dataTestSubjCross)).toHaveStyle('opacity: 0'); + }); + + it("renders unknown for the user's information", () => { + appMockRender.render( + <UserRepresentation {...{ ...defaultProps, assignee: { uid: 'unknownId' } }} /> + ); + + expect(screen.getByText('Unknown')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_profiles/user_representation.tsx b/x-pack/plugins/cases/public/components/user_profiles/user_representation.tsx new file mode 100644 index 00000000000000..8ca7fdd435fc48 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_profiles/user_representation.tsx @@ -0,0 +1,103 @@ +/* + * 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, { useCallback, useState } from 'react'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; +import { UserProfileWithAvatar } from '@kbn/user-profile-components'; +import { CaseUserAvatar } from './user_avatar'; +import { UserToolTip } from './user_tooltip'; +import { getName } from './display_name'; +import * as i18n from './translations'; +import { Assignee } from './types'; +import { useCasesContext } from '../cases_context/use_cases_context'; + +const UserAvatarWithName: React.FC<{ profile?: UserProfileWithAvatar }> = ({ profile }) => { + return ( + <EuiFlexGroup alignItems="center" gutterSize="s"> + <EuiFlexItem grow={false}> + <CaseUserAvatar size={'s'} profile={profile} /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFlexGroup direction={'column'} gutterSize="none"> + <EuiFlexItem> + <EuiText size="s" className="eui-textBreakWord"> + {getName(profile?.user)} + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; +UserAvatarWithName.displayName = 'UserAvatarWithName'; + +export interface UserRepresentationProps { + assignee: Assignee; + onRemoveAssignee: (removedAssigneeUID: string) => void; +} + +const UserRepresentationComponent: React.FC<UserRepresentationProps> = ({ + assignee, + onRemoveAssignee, +}) => { + const { permissions } = useCasesContext(); + const [isHovering, setIsHovering] = useState(false); + + const removeAssigneeCallback = useCallback( + () => onRemoveAssignee(assignee.uid), + [onRemoveAssignee, assignee.uid] + ); + + const onFocus = useCallback(() => setIsHovering(true), []); + const onFocusLeave = useCallback(() => setIsHovering(false), []); + + const usernameDataTestSubj = assignee.profile?.user.username ?? assignee.uid; + + return ( + <EuiFlexGroup + onMouseEnter={onFocus} + onMouseLeave={onFocusLeave} + alignItems="center" + gutterSize="s" + justifyContent="spaceBetween" + data-test-subj={`user-profile-assigned-user-group-${usernameDataTestSubj}`} + > + <EuiFlexItem grow={false}> + <UserToolTip profile={assignee.profile}> + <UserAvatarWithName profile={assignee.profile} /> + </UserToolTip> + </EuiFlexItem> + {permissions.update && ( + <EuiFlexItem grow={false}> + <EuiToolTip + position="left" + content={i18n.REMOVE_ASSIGNEE} + data-test-subj={`user-profile-assigned-user-cross-tooltip-${usernameDataTestSubj}`} + > + <EuiButtonIcon + css={{ + opacity: isHovering ? 1 : 0, + }} + onFocus={onFocus} + onBlur={onFocusLeave} + data-test-subj={`user-profile-assigned-user-cross-${usernameDataTestSubj}`} + aria-label={i18n.REMOVE_ASSIGNEE_ARIA_LABEL} + iconType="cross" + color="danger" + iconSize="m" + onClick={removeAssigneeCallback} + /> + </EuiToolTip> + </EuiFlexItem> + )} + </EuiFlexGroup> + ); +}; + +UserRepresentationComponent.displayName = 'UserRepresentation'; + +export const UserRepresentation = React.memo(UserRepresentationComponent); diff --git a/x-pack/plugins/cases/public/components/user_profiles/user_tooltip.test.tsx b/x-pack/plugins/cases/public/components/user_profiles/user_tooltip.test.tsx new file mode 100644 index 00000000000000..17d26a39c48f6d --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_profiles/user_tooltip.test.tsx @@ -0,0 +1,174 @@ +/* + * 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 { UserProfileWithAvatar } from '@kbn/user-profile-components'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import React from 'react'; +import { UserToolTip } from './user_tooltip'; + +describe('UserToolTip', () => { + it('renders the tooltip when hovering', async () => { + const profile: UserProfileWithAvatar = { + uid: '1', + enabled: true, + data: { + avatar: { + initials: 'SU', + }, + }, + user: { + username: 'user', + email: 'some.user@google.com', + full_name: 'Some Super User', + }, + }; + + render( + <UserToolTip profile={profile}> + <strong>{'case user'}</strong> + </UserToolTip> + ); + + fireEvent.mouseOver(screen.getByText('case user')); + + await waitFor(() => screen.getByTestId('user-profile-tooltip')); + expect(screen.getByText('Some Super User')).toBeInTheDocument(); + expect(screen.getByText('some.user@google.com')).toBeInTheDocument(); + expect(screen.getByText('SU')).toBeInTheDocument(); + }); + + it('only shows the display name if full name is missing', async () => { + const profile: UserProfileWithAvatar = { + uid: '1', + enabled: true, + data: { + avatar: { + initials: 'SU', + }, + }, + user: { + username: 'user', + email: 'some.user@google.com', + }, + }; + + render( + <UserToolTip profile={profile}> + <strong>{'case user'}</strong> + </UserToolTip> + ); + + fireEvent.mouseOver(screen.getByText('case user')); + + await waitFor(() => screen.getByTestId('user-profile-tooltip')); + expect(screen.queryByText('Some Super User')).not.toBeInTheDocument(); + expect(screen.getByText('some.user@google.com')).toBeInTheDocument(); + expect(screen.getByText('SU')).toBeInTheDocument(); + }); + + it('only shows the full name if display name is missing', async () => { + const profile: UserProfileWithAvatar = { + uid: '1', + enabled: true, + data: { + avatar: { + initials: 'SU', + }, + }, + user: { + username: 'user', + full_name: 'Some Super User', + email: 'some.user@google.com', + }, + }; + + render( + <UserToolTip profile={profile}> + <strong>{'case user'}</strong> + </UserToolTip> + ); + + fireEvent.mouseOver(screen.getByText('case user')); + + await waitFor(() => screen.getByTestId('user-profile-tooltip')); + expect(screen.getByText('Some Super User')).toBeInTheDocument(); + expect(screen.getByText('some.user@google.com')).toBeInTheDocument(); + expect(screen.getByText('SU')).toBeInTheDocument(); + }); + + it('only shows the email once when display name and full name are not defined', async () => { + const profile: UserProfileWithAvatar = { + uid: '1', + enabled: true, + data: { + avatar: { + initials: 'SU', + }, + }, + user: { + username: 'user', + email: 'some.user@google.com', + }, + }; + + render( + <UserToolTip profile={profile}> + <strong>{'case user'}</strong> + </UserToolTip> + ); + + fireEvent.mouseOver(screen.getByText('case user')); + + await waitFor(() => screen.getByTestId('user-profile-tooltip')); + expect(screen.queryByText('Some Super User')).not.toBeInTheDocument(); + expect(screen.getByText('some.user@google.com')).toBeInTheDocument(); + expect(screen.getByText('SU')).toBeInTheDocument(); + }); + + it('only shows the username once when all other fields are undefined', async () => { + const profile: UserProfileWithAvatar = { + uid: '1', + enabled: true, + data: { + avatar: { + initials: 'SU', + }, + }, + user: { + username: 'user', + }, + }; + + render( + <UserToolTip profile={profile}> + <strong>{'case user'}</strong> + </UserToolTip> + ); + + fireEvent.mouseOver(screen.getByText('case user')); + + await waitFor(() => screen.getByTestId('user-profile-tooltip')); + expect(screen.queryByText('Some Super User')).not.toBeInTheDocument(); + expect(screen.queryByText('some.user@google.com')).not.toBeInTheDocument(); + expect(screen.getByText('user')).toBeInTheDocument(); + expect(screen.getByText('SU')).toBeInTheDocument(); + }); + + it('shows an unknown users display name and avatar', async () => { + render( + <UserToolTip> + <strong>{'case user'}</strong> + </UserToolTip> + ); + + fireEvent.mouseOver(screen.getByText('case user')); + + await waitFor(() => screen.getByTestId('user-profile-tooltip')); + expect(screen.getByText('Unable to find user profile')).toBeInTheDocument(); + expect(screen.getByText('?')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_profiles/user_tooltip.tsx b/x-pack/plugins/cases/public/components/user_profiles/user_tooltip.tsx new file mode 100644 index 00000000000000..9c837997b9a1fb --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_profiles/user_tooltip.tsx @@ -0,0 +1,105 @@ +/* + * 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 from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; +import { UserProfileUserInfo, UserProfileWithAvatar } from '@kbn/user-profile-components'; +import { CaseUserAvatar } from './user_avatar'; +import { getName } from './display_name'; +import * as i18n from './translations'; + +const UserFullInformation: React.FC<{ profile?: UserProfileWithAvatar }> = React.memo( + ({ profile }) => { + if (profile?.user.full_name) { + return ( + <EuiText size="s" className="eui-textBreakWord"> + <strong data-test-subj="user-profile-tooltip-full-name">{profile.user.full_name}</strong> + </EuiText> + ); + } + + return ( + <EuiText + size="s" + className="eui-textBreakWord" + data-test-subj="user-profile-tooltip-single-name" + > + <strong>{getNameOrMissingText(profile?.user)}</strong> + </EuiText> + ); + } +); + +const getNameOrMissingText = (user?: UserProfileUserInfo) => { + if (!user) { + return i18n.MISSING_PROFILE; + } + + return getName(user); +}; + +UserFullInformation.displayName = 'UserFullInformation'; + +interface UserFullRepresentationProps { + profile?: UserProfileWithAvatar; +} + +const UserFullRepresentationComponent: React.FC<UserFullRepresentationProps> = ({ profile }) => { + return ( + <EuiFlexGroup alignItems="center" gutterSize="s"> + <EuiFlexItem grow={false} data-test-subj="user-profile-tooltip-avatar"> + <CaseUserAvatar size={'m'} profile={profile} /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFlexGroup direction={'column'} gutterSize="none"> + <EuiFlexItem> + <UserFullInformation profile={profile} /> + </EuiFlexItem> + {profile && displayEmail(profile) && ( + <EuiFlexItem grow={false}> + <EuiText + size="s" + className="eui-textBreakWord" + data-test-subj="user-profile-tooltip-email" + > + {profile.user.email} + </EuiText> + </EuiFlexItem> + )} + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; + +UserFullRepresentationComponent.displayName = 'UserFullRepresentation'; + +const displayEmail = (profile?: UserProfileWithAvatar) => { + return profile?.user.full_name && profile?.user.email; +}; + +export interface UserToolTipProps { + children: React.ReactElement; + profile?: UserProfileWithAvatar; +} + +const UserToolTipComponent: React.FC<UserToolTipProps> = ({ children, profile }) => { + return ( + <EuiToolTip + display="inlineBlock" + position="top" + content={<UserFullRepresentationComponent profile={profile} />} + data-test-subj="user-profile-tooltip" + > + {children} + </EuiToolTip> + ); +}; + +UserToolTipComponent.displayName = 'UserToolTip'; +export const UserToolTip = React.memo(UserToolTipComponent); diff --git a/x-pack/plugins/cases/public/containers/__mocks__/api.ts b/x-pack/plugins/cases/public/containers/__mocks__/api.ts index f781daac156976..2b431351230808 100644 --- a/x-pack/plugins/cases/public/containers/__mocks__/api.ts +++ b/x-pack/plugins/cases/public/containers/__mocks__/api.ts @@ -26,7 +26,6 @@ import { casesStatus, caseUserActions, pushedCase, - respReporters, tags, } from '../mock'; import { ResolvedCase, SeverityAll } from '../../../common/ui/types'; @@ -34,11 +33,12 @@ import { CasePatchRequest, CasePostRequest, CommentRequest, - User, CaseStatuses, SingleCaseMetricsResponse, } from '../../../common/api'; import type { ValidFeatureId } from '@kbn/rule-data-utils'; +import { UserProfile } from '@kbn/security-plugin/common'; +import { userProfiles } from '../user_profiles/api.mock'; export const getCase = async ( caseId: string, @@ -62,8 +62,7 @@ export const getCasesStatus = async (signal: AbortSignal): Promise<CasesStatus> export const getTags = async (signal: AbortSignal): Promise<string[]> => Promise.resolve(tags); -export const getReporters = async (signal: AbortSignal): Promise<User[]> => - Promise.resolve(respReporters); +export const findAssignees = async (): Promise<UserProfile[]> => userProfiles; export const getCaseUserActions = async ( caseId: string, @@ -75,6 +74,7 @@ export const getCases = async ({ severity: SeverityAll, search: '', searchFields: [], + assignees: [], reporters: [], status: CaseStatuses.open, tags: [], diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index 45cde4c4f94cba..e51224dc593dcb 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -23,7 +23,6 @@ import { getCase, getCases, getCaseUserActions, - getReporters, getTags, patchCase, patchCasesStatus, @@ -48,8 +47,6 @@ import { cases, caseUserActions, pushedCase, - reporters, - respReporters, tags, caseUserActionsSnake, casesStatusSnake, @@ -200,6 +197,7 @@ describe('Cases API', () => { query: { ...DEFAULT_QUERY_PARAMS, searchFields: DEFAULT_FILTER_OPTIONS.searchFields, + assignees: [], reporters: [], tags: [], owner: [SECURITY_SOLUTION_OWNER], @@ -212,7 +210,8 @@ describe('Cases API', () => { await getCases({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, - reporters: [...respReporters, { username: null, full_name: null, email: null }], + assignees: ['123'], + reporters: [{ username: 'username', full_name: null, email: null }], tags, status: CaseStatuses.open, search: 'hello', @@ -225,7 +224,8 @@ describe('Cases API', () => { method: 'GET', query: { ...DEFAULT_QUERY_PARAMS, - reporters, + assignees: ['123'], + reporters: ['username'], tags: ['coke', 'pepsi'], search: 'hello', searchFields: DEFAULT_FILTER_OPTIONS.searchFields, @@ -250,6 +250,7 @@ describe('Cases API', () => { query: { ...DEFAULT_QUERY_PARAMS, searchFields: DEFAULT_FILTER_OPTIONS.searchFields, + assignees: [], reporters: [], tags: [], severity: CaseSeverity.HIGH, @@ -272,6 +273,7 @@ describe('Cases API', () => { query: { ...DEFAULT_QUERY_PARAMS, searchFields: DEFAULT_FILTER_OPTIONS.searchFields, + assignees: [], reporters: [], tags: [], }, @@ -285,7 +287,8 @@ describe('Cases API', () => { await getCases({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, - reporters: [...respReporters, { username: null, full_name: null, email: null }], + assignees: ['123'], + reporters: [{ username: undefined, full_name: undefined, email: undefined }], tags: weirdTags, status: CaseStatuses.open, search: 'hello', @@ -298,7 +301,8 @@ describe('Cases API', () => { method: 'GET', query: { ...DEFAULT_QUERY_PARAMS, - reporters, + assignees: ['123'], + reporters: [], tags: ['(', '"double"'], search: 'hello', searchFields: DEFAULT_FILTER_OPTIONS.searchFields, @@ -378,29 +382,6 @@ describe('Cases API', () => { }); }); - describe('getReporters', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(respReporters); - }); - - test('should be called with correct check url, method, signal', async () => { - await getReporters(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); - expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/reporters`, { - method: 'GET', - signal: abortCtrl.signal, - query: { - owner: [SECURITY_SOLUTION_OWNER], - }, - }); - }); - - test('should return correct response', async () => { - const resp = await getReporters(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); - expect(resp).toEqual(respReporters); - }); - }); - describe('getTags', () => { beforeEach(() => { fetchMock.mockClear(); diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index 70b9c4033a424c..2b7e8910fb9daf 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -164,6 +164,7 @@ export const getCases = async ({ search: '', searchFields: [], severity: SeverityAll, + assignees: [], reporters: [], status: StatusAll, tags: [], @@ -180,6 +181,7 @@ export const getCases = async ({ const query = { ...(filterOptions.status !== StatusAll ? { status: filterOptions.status } : {}), ...(filterOptions.severity !== SeverityAll ? { severity: filterOptions.severity } : {}), + assignees: filterOptions.assignees, reporters: filterOptions.reporters.map((r) => r.username ?? '').filter((r) => r !== ''), tags: filterOptions.tags, ...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}), diff --git a/x-pack/plugins/cases/public/containers/constants.ts b/x-pack/plugins/cases/public/containers/constants.ts index 3a04b411cb8e74..a87d7733034474 100644 --- a/x-pack/plugins/cases/public/containers/constants.ts +++ b/x-pack/plugins/cases/public/containers/constants.ts @@ -24,3 +24,4 @@ export const CASE_TAGS_CACHE_KEY = 'case-tags'; export const USER_PROFILES_CACHE_KEY = 'user-profiles'; export const USER_PROFILES_SUGGEST_CACHE_KEY = 'suggest'; export const USER_PROFILES_BULK_GET_CACHE_KEY = 'bulk-get'; +export const USER_PROFILES_GET_CURRENT_CACHE_KEY = 'get-current'; diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index 92e601dd0c9e9c..812349e96fce73 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -228,7 +228,8 @@ export const basicCase: Case = { settings: { syncAlerts: true, }, - assignees: [], + // damaged_raccoon uid + assignees: [{ uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0' }], }; export const caseWithAlerts = { @@ -553,13 +554,6 @@ export const pushedCaseSnake = { external_service: { ...basicPushSnake, connector_id: pushConnectorId }, }; -export const reporters: string[] = ['alexis', 'kim', 'maria', 'steph']; -export const respReporters = [ - { username: 'alexis', full_name: null, email: null }, - { username: 'kim', full_name: null, email: null }, - { username: 'maria', full_name: null, email: null }, - { username: 'steph', full_name: null, email: null }, -]; export const casesSnake: CasesResponse = [ basicCaseSnake, { ...pushedCaseSnake, id: '1', totalComment: 0, comments: [] }, @@ -688,6 +682,19 @@ export const getUserAction = ( payload: { title: 'a title' }, ...overrides, }; + case ActionTypes.assignees: + return { + ...commonProperties, + type: ActionTypes.assignees, + payload: { + assignees: [ + // These values map to uids in x-pack/plugins/cases/public/containers/user_profiles/api.mock.ts + { uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0' }, + { uid: 'u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0' }, + ], + }, + ...overrides, + }; default: return { diff --git a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.test.tsx b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.test.tsx index c5dbf017da8c94..a9d80181b58f75 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.test.tsx @@ -62,6 +62,7 @@ describe('useGetCaseUserActions', () => { caseServices: {}, hasDataToPush: true, participants: [elasticUser], + profileUids: new Set(), }, isError: false, isLoading: false, @@ -87,6 +88,84 @@ describe('useGetCaseUserActions', () => { expect(addError).toHaveBeenCalled(); }); + describe('getProfileUids', () => { + it('aggregates the uids from an assignment add user action', async () => { + jest + .spyOn(api, 'getCaseUserActions') + .mockReturnValue( + Promise.resolve([...caseUserActions, getUserAction('assignees', Actions.add)]) + ); + + await act(async () => { + const { result } = renderHook<string, UseGetCaseUserActions>( + () => useGetCaseUserActions(basicCase.id, basicCase.connector.id), + { wrapper } + ); + + await waitFor(() => { + expect(result.current.data?.profileUids).toMatchInlineSnapshot(` + Set { + "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0", + "u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0", + } + `); + }); + }); + }); + + it('ignores duplicate uids', async () => { + jest + .spyOn(api, 'getCaseUserActions') + .mockReturnValue( + Promise.resolve([ + ...caseUserActions, + getUserAction('assignees', Actions.add), + getUserAction('assignees', Actions.add), + ]) + ); + + await act(async () => { + const { result } = renderHook<string, UseGetCaseUserActions>( + () => useGetCaseUserActions(basicCase.id, basicCase.connector.id), + { wrapper } + ); + + await waitFor(() => { + expect(result.current.data?.profileUids).toMatchInlineSnapshot(` + Set { + "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0", + "u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0", + } + `); + }); + }); + }); + + it('aggregates the uids from an assignment delete user action', async () => { + jest + .spyOn(api, 'getCaseUserActions') + .mockReturnValue( + Promise.resolve([...caseUserActions, getUserAction('assignees', Actions.delete)]) + ); + + await act(async () => { + const { result } = renderHook<string, UseGetCaseUserActions>( + () => useGetCaseUserActions(basicCase.id, basicCase.connector.id), + { wrapper } + ); + + await waitFor(() => { + expect(result.current.data?.profileUids).toMatchInlineSnapshot(` + Set { + "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0", + "u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0", + } + `); + }); + }); + }); + }); + describe('getPushedInfo', () => { it('Correctly marks first/last index - hasDataToPush: false', () => { const userActions = [...caseUserActions, getUserAction('pushed', Actions.push_to_service)]; diff --git a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx index da695201d6d768..1d36521d0b6f41 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx @@ -208,6 +208,21 @@ export const getPushedInfo = ( }; }; +export const getProfileUids = (userActions: CaseUserActions[]) => { + const uids = userActions.reduce<Set<string>>((acc, userAction) => { + if (userAction.type === ActionTypes.assignees) { + const uidsFromPayload = userAction.payload.assignees.map((assignee) => assignee.uid); + for (const uid of uidsFromPayload) { + acc.add(uid); + } + } + + return acc; + }, new Set()); + + return uids; +}; + export const useGetCaseUserActions = (caseId: string, caseConnectorId: string) => { const toasts = useToasts(); const abortCtrlRef = new AbortController(); @@ -221,9 +236,12 @@ export const useGetCaseUserActions = (caseId: string, caseConnectorId: string) = const caseUserActions = !isEmpty(response) ? response : []; const pushedInfo = getPushedInfo(caseUserActions, caseConnectorId); + const profileUids = getProfileUids(caseUserActions); + return { caseUserActions, participants, + profileUids, ...pushedInfo, }; }, diff --git a/x-pack/plugins/cases/public/containers/use_get_cases.tsx b/x-pack/plugins/cases/public/containers/use_get_cases.tsx index ce19e68fa17988..7b046cac3f13fe 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases.tsx @@ -19,6 +19,7 @@ export const DEFAULT_FILTER_OPTIONS: FilterOptions = { search: '', searchFields: DEFAULT_SEARCH_FIELDS, severity: SeverityAll, + assignees: [], reporters: [], status: StatusAll, tags: [], diff --git a/x-pack/plugins/cases/public/containers/use_get_cases_metrics.test.tsx b/x-pack/plugins/cases/public/containers/use_get_cases_metrics.test.tsx index 6601a104d9f7d0..a8747a2bd43a55 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases_metrics.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases_metrics.test.tsx @@ -15,7 +15,7 @@ import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; jest.mock('../api'); jest.mock('../common/lib/kibana'); -describe('useGetReporters', () => { +describe('useGetCasesMetrics', () => { beforeEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); diff --git a/x-pack/plugins/cases/public/containers/use_get_reporters.test.tsx b/x-pack/plugins/cases/public/containers/use_get_reporters.test.tsx deleted file mode 100644 index 38d47d3aa9cbbc..00000000000000 --- a/x-pack/plugins/cases/public/containers/use_get_reporters.test.tsx +++ /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 React from 'react'; -import { renderHook, act } from '@testing-library/react-hooks'; -import { useGetReporters, UseGetReporters } from './use_get_reporters'; -import { reporters, respReporters } from './mock'; -import * as api from './api'; -import { TestProviders } from '../common/mock'; -import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; - -jest.mock('./api'); -jest.mock('../common/lib/kibana'); - -describe('useGetReporters', () => { - const abortCtrl = new AbortController(); - beforeEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('init', async () => { - const { result } = renderHook<string, UseGetReporters>(() => useGetReporters(), { - wrapper: ({ children }) => <TestProviders>{children}</TestProviders>, - }); - - await act(async () => { - expect(result.current).toEqual({ - reporters: [], - respReporters: [], - isLoading: true, - isError: false, - fetchReporters: result.current.fetchReporters, - }); - }); - }); - - it('calls getReporters api', async () => { - const spyOnGetReporters = jest.spyOn(api, 'getReporters'); - await act(async () => { - const { waitForNextUpdate } = renderHook<string, UseGetReporters>(() => useGetReporters(), { - wrapper: ({ children }) => <TestProviders>{children}</TestProviders>, - }); - await waitForNextUpdate(); - expect(spyOnGetReporters).toBeCalledWith(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); - }); - }); - - it('fetch reporters', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseGetReporters>( - () => useGetReporters(), - { - wrapper: ({ children }) => <TestProviders>{children}</TestProviders>, - } - ); - await waitForNextUpdate(); - expect(result.current).toEqual({ - reporters, - respReporters, - isLoading: false, - isError: false, - fetchReporters: result.current.fetchReporters, - }); - }); - }); - - it('refetch reporters', async () => { - const spyOnGetReporters = jest.spyOn(api, 'getReporters'); - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseGetReporters>( - () => useGetReporters(), - { - wrapper: ({ children }) => <TestProviders>{children}</TestProviders>, - } - ); - await waitForNextUpdate(); - result.current.fetchReporters(); - expect(spyOnGetReporters).toHaveBeenCalledTimes(2); - }); - }); - - it('unhappy path', async () => { - const spyOnGetReporters = jest.spyOn(api, 'getReporters'); - spyOnGetReporters.mockImplementation(() => { - throw new Error('Something went wrong'); - }); - - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseGetReporters>( - () => useGetReporters(), - { - wrapper: ({ children }) => <TestProviders>{children}</TestProviders>, - } - ); - await waitForNextUpdate(); - - expect(result.current).toEqual({ - reporters: [], - respReporters: [], - isLoading: false, - isError: true, - fetchReporters: result.current.fetchReporters, - }); - }); - }); -}); diff --git a/x-pack/plugins/cases/public/containers/use_get_reporters.tsx b/x-pack/plugins/cases/public/containers/use_get_reporters.tsx deleted file mode 100644 index ce8aa4b961c230..00000000000000 --- a/x-pack/plugins/cases/public/containers/use_get_reporters.tsx +++ /dev/null @@ -1,95 +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 { useCallback, useEffect, useState, useRef } from 'react'; -import { isEmpty } from 'lodash/fp'; - -import { User } from '../../common/api'; -import { getReporters } from './api'; -import * as i18n from './translations'; -import { useToasts } from '../common/lib/kibana'; -import { useCasesContext } from '../components/cases_context/use_cases_context'; - -interface ReportersState { - reporters: string[]; - respReporters: User[]; - isLoading: boolean; - isError: boolean; -} - -const initialData: ReportersState = { - reporters: [], - respReporters: [], - isLoading: true, - isError: false, -}; - -export interface UseGetReporters extends ReportersState { - fetchReporters: () => void; -} - -export const useGetReporters = (): UseGetReporters => { - const { owner } = useCasesContext(); - const [reportersState, setReporterState] = useState<ReportersState>(initialData); - - const toasts = useToasts(); - const isCancelledRef = useRef(false); - const abortCtrlRef = useRef(new AbortController()); - - const fetchReporters = useCallback(async () => { - try { - isCancelledRef.current = false; - abortCtrlRef.current.abort(); - abortCtrlRef.current = new AbortController(); - setReporterState({ - ...reportersState, - isLoading: true, - }); - - const response = await getReporters(abortCtrlRef.current.signal, owner); - const myReporters = response - .map((r) => (r.full_name == null || isEmpty(r.full_name) ? r.username ?? '' : r.full_name)) - .filter((u) => !isEmpty(u)); - - if (!isCancelledRef.current) { - setReporterState({ - reporters: myReporters, - respReporters: response, - isLoading: false, - isError: false, - }); - } - } catch (error) { - if (!isCancelledRef.current) { - if (error.name !== 'AbortError') { - toasts.addError( - error.body && error.body.message ? new Error(error.body.message) : error, - { title: i18n.ERROR_TITLE } - ); - } - - setReporterState({ - reporters: [], - respReporters: [], - isLoading: false, - isError: true, - }); - } - } - }, [owner, reportersState, toasts]); - - useEffect(() => { - fetchReporters(); - return () => { - isCancelledRef.current = true; - abortCtrlRef.current.abort(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return { ...reportersState, fetchReporters }; -}; diff --git a/x-pack/plugins/cases/public/containers/user_profiles/__mocks__/api.ts b/x-pack/plugins/cases/public/containers/user_profiles/__mocks__/api.ts index 36c88451124ca1..6901852a405fa2 100644 --- a/x-pack/plugins/cases/public/containers/user_profiles/__mocks__/api.ts +++ b/x-pack/plugins/cases/public/containers/user_profiles/__mocks__/api.ts @@ -8,8 +8,8 @@ import { UserProfile } from '@kbn/security-plugin/common'; import { userProfiles } from '../api.mock'; -export const suggestUserProfiles = async (): Promise<UserProfile[]> => - Promise.resolve(userProfiles); +export const suggestUserProfiles = async (): Promise<UserProfile[]> => userProfiles; -export const bulkGetUserProfiles = async (): Promise<UserProfile[]> => - Promise.resolve(userProfiles); +export const bulkGetUserProfiles = async (): Promise<UserProfile[]> => userProfiles; + +export const getCurrentUserProfile = async (): Promise<UserProfile> => userProfiles[0]; diff --git a/x-pack/plugins/cases/public/containers/user_profiles/api.mock.ts b/x-pack/plugins/cases/public/containers/user_profiles/api.mock.ts index e9382f7092ae01..1296cf98788274 100644 --- a/x-pack/plugins/cases/public/containers/user_profiles/api.mock.ts +++ b/x-pack/plugins/cases/public/containers/user_profiles/api.mock.ts @@ -41,3 +41,5 @@ export const userProfiles: UserProfile[] = [ ]; export const userProfilesIds = userProfiles.map((profile) => profile.uid); + +export const userProfilesMap = new Map(userProfiles.map((profile) => [profile.uid, profile])); diff --git a/x-pack/plugins/cases/public/containers/user_profiles/api.test.ts b/x-pack/plugins/cases/public/containers/user_profiles/api.test.ts index 7234cc9fb54fe7..0f7c9d9c31fa96 100644 --- a/x-pack/plugins/cases/public/containers/user_profiles/api.test.ts +++ b/x-pack/plugins/cases/public/containers/user_profiles/api.test.ts @@ -5,9 +5,11 @@ * 2.0. */ +import { securityMock } from '@kbn/security-plugin/public/mocks'; +import { SecurityPluginStart } from '@kbn/security-plugin/public'; import { GENERAL_CASES_OWNER } from '../../../common/constants'; import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock'; -import { bulkGetUserProfiles, suggestUserProfiles } from './api'; +import { bulkGetUserProfiles, getCurrentUserProfile, suggestUserProfiles } from './api'; import { userProfiles, userProfilesIds } from './api.mock'; describe('User profiles API', () => { @@ -24,7 +26,7 @@ describe('User profiles API', () => { const res = await suggestUserProfiles({ http, name: 'elastic', - owner: [GENERAL_CASES_OWNER], + owners: [GENERAL_CASES_OWNER], signal: abortCtrl.signal, }); @@ -35,22 +37,23 @@ describe('User profiles API', () => { await suggestUserProfiles({ http, name: 'elastic', - owner: [GENERAL_CASES_OWNER], + owners: [GENERAL_CASES_OWNER], signal: abortCtrl.signal, }); expect(http.post).toHaveBeenCalledWith('/internal/cases/_suggest_user_profiles', { - body: '{"name":"elastic","size":10,"owner":["cases"]}', + body: '{"name":"elastic","size":10,"owners":["cases"]}', signal: abortCtrl.signal, }); }); }); describe('bulkGetUserProfiles', () => { - const { security } = createStartServicesMock(); + let security: SecurityPluginStart; beforeEach(() => { jest.clearAllMocks(); + security = securityMock.createStart(); security.userProfiles.bulkGet = jest.fn().mockResolvedValue(userProfiles); }); @@ -63,7 +66,7 @@ describe('User profiles API', () => { expect(res).toEqual(userProfiles); }); - it('calls http.post correctly', async () => { + it('calls bulkGet correctly', async () => { await bulkGetUserProfiles({ security, uids: userProfilesIds, @@ -79,4 +82,34 @@ describe('User profiles API', () => { }); }); }); + + describe('getCurrentUserProfile', () => { + let security: SecurityPluginStart; + + const currentProfile = userProfiles[0]; + + beforeEach(() => { + jest.clearAllMocks(); + security = securityMock.createStart(); + security.userProfiles.getCurrent = jest.fn().mockResolvedValue(currentProfile); + }); + + it('returns the current user profile correctly', async () => { + const res = await getCurrentUserProfile({ + security, + }); + + expect(res).toEqual(currentProfile); + }); + + it('calls getCurrent correctly', async () => { + await getCurrentUserProfile({ + security, + }); + + expect(security.userProfiles.getCurrent).toHaveBeenCalledWith({ + dataPath: 'avatar', + }); + }); + }); }); diff --git a/x-pack/plugins/cases/public/containers/user_profiles/api.ts b/x-pack/plugins/cases/public/containers/user_profiles/api.ts index 6da84d19914238..cfd1c04d0afbca 100644 --- a/x-pack/plugins/cases/public/containers/user_profiles/api.ts +++ b/x-pack/plugins/cases/public/containers/user_profiles/api.ts @@ -13,7 +13,7 @@ import { INTERNAL_SUGGEST_USER_PROFILES_URL, DEFAULT_USER_SIZE } from '../../../ export interface SuggestUserProfilesArgs { http: HttpStart; name: string; - owner: string[]; + owners: string[]; signal: AbortSignal; size?: number; } @@ -22,11 +22,11 @@ export const suggestUserProfiles = async ({ http, name, size = DEFAULT_USER_SIZE, - owner, + owners, signal, }: SuggestUserProfilesArgs): Promise<UserProfile[]> => { const response = await http.post<UserProfile[]>(INTERNAL_SUGGEST_USER_PROFILES_URL, { - body: JSON.stringify({ name, size, owner }), + body: JSON.stringify({ name, size, owners }), signal, }); @@ -42,5 +42,19 @@ export const bulkGetUserProfiles = async ({ security, uids, }: BulkGetUserProfilesArgs): Promise<UserProfile[]> => { + if (uids.length === 0) { + return []; + } + return security.userProfiles.bulkGet({ uids: new Set(uids), dataPath: 'avatar' }); }; + +export interface GetCurrentUserProfileArgs { + security: SecurityPluginStart; +} + +export const getCurrentUserProfile = async ({ + security, +}: GetCurrentUserProfileArgs): Promise<UserProfile> => { + return security.userProfiles.getCurrent({ dataPath: 'avatar' }); +}; diff --git a/x-pack/plugins/cases/public/containers/user_profiles/use_assignees.test.ts b/x-pack/plugins/cases/public/containers/user_profiles/use_assignees.test.ts new file mode 100644 index 00000000000000..db4527ae31e436 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/user_profiles/use_assignees.test.ts @@ -0,0 +1,113 @@ +/* + * 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 { UserProfileWithAvatar } from '@kbn/user-profile-components'; +import { renderHook } from '@testing-library/react-hooks'; +import { userProfiles, userProfilesMap } from './api.mock'; +import { useAssignees } from './use_assignees'; + +describe('useAssignees', () => { + it('returns an empty array when the caseAssignees is empty', () => { + const { result } = renderHook(() => + useAssignees({ caseAssignees: [], userProfiles: new Map(), currentUserProfile: undefined }) + ); + + expect(result.current.allAssignees).toHaveLength(0); + expect(result.current.assigneesWithProfiles).toHaveLength(0); + expect(result.current.assigneesWithoutProfiles).toHaveLength(0); + }); + + it('returns all items in the with profiles array when they have profiles', () => { + const { result } = renderHook(() => + useAssignees({ + caseAssignees: userProfiles.map((profile) => ({ uid: profile.uid })), + userProfiles: userProfilesMap, + currentUserProfile: undefined, + }) + ); + + expect(result.current.assigneesWithoutProfiles).toHaveLength(0); + expect(result.current.allAssignees).toEqual(result.current.assigneesWithProfiles); + expect(result.current.allAssignees).toEqual(userProfiles.map(asAssigneeWithProfile)); + }); + + it('returns a sorted list of assignees with profiles', () => { + const unsorted = [...userProfiles].reverse(); + const { result } = renderHook(() => + useAssignees({ + caseAssignees: unsorted.map((profile) => ({ uid: profile.uid })), + userProfiles: userProfilesMap, + currentUserProfile: undefined, + }) + ); + + expect(result.current.assigneesWithoutProfiles).toHaveLength(0); + expect(result.current.allAssignees).toEqual(result.current.assigneesWithProfiles); + expect(result.current.allAssignees).toEqual(userProfiles.map(asAssigneeWithProfile)); + }); + + it('returns all items in the without profiles array when they do not have profiles', () => { + const unknownProfiles = [{ uid: '1' }, { uid: '2' }]; + const { result } = renderHook(() => + useAssignees({ + caseAssignees: unknownProfiles, + userProfiles: userProfilesMap, + currentUserProfile: undefined, + }) + ); + + expect(result.current.assigneesWithoutProfiles).toHaveLength(2); + expect(result.current.assigneesWithoutProfiles).toEqual(unknownProfiles); + expect(result.current.allAssignees).toEqual(unknownProfiles); + }); + + it('returns 1 user with a valid profile and 1 user with no profile and combines them in the all field', () => { + const assignees = [{ uid: '1' }, { uid: userProfiles[0].uid }]; + const { result } = renderHook(() => + useAssignees({ + caseAssignees: assignees, + userProfiles: userProfilesMap, + currentUserProfile: undefined, + }) + ); + + expect(result.current.assigneesWithProfiles).toHaveLength(1); + expect(result.current.assigneesWithoutProfiles).toHaveLength(1); + expect(result.current.allAssignees).toHaveLength(2); + + expect(result.current.assigneesWithProfiles).toEqual([asAssigneeWithProfile(userProfiles[0])]); + expect(result.current.assigneesWithoutProfiles).toEqual([{ uid: '1' }]); + expect(result.current.allAssignees).toEqual([ + asAssigneeWithProfile(userProfiles[0]), + { uid: '1' }, + ]); + }); + + it('returns assignees with profiles with the current user at the front', () => { + const { result } = renderHook(() => + useAssignees({ + caseAssignees: userProfiles, + userProfiles: userProfilesMap, + currentUserProfile: userProfiles[2], + }) + ); + + expect(result.current.assigneesWithProfiles).toHaveLength(3); + expect(result.current.allAssignees).toHaveLength(3); + + const asAssignees = userProfiles.map(asAssigneeWithProfile); + + expect(result.current.assigneesWithProfiles).toEqual([ + asAssignees[2], + asAssignees[0], + asAssignees[1], + ]); + expect(result.current.allAssignees).toEqual([asAssignees[2], asAssignees[0], asAssignees[1]]); + }); +}); + +const asAssigneeWithProfile = (profile: UserProfileWithAvatar) => ({ uid: profile.uid, profile }); diff --git a/x-pack/plugins/cases/public/containers/user_profiles/use_assignees.ts b/x-pack/plugins/cases/public/containers/user_profiles/use_assignees.ts new file mode 100644 index 00000000000000..2e1bb0a61dbdab --- /dev/null +++ b/x-pack/plugins/cases/public/containers/user_profiles/use_assignees.ts @@ -0,0 +1,68 @@ +/* + * 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 { UserProfileWithAvatar } from '@kbn/user-profile-components'; +import { useMemo } from 'react'; +import { CaseAssignees } from '../../../common/api'; +import { CurrentUserProfile } from '../../components/types'; +import { bringCurrentUserToFrontAndSort } from '../../components/user_profiles/sort'; +import { Assignee, AssigneeWithProfile } from '../../components/user_profiles/types'; + +interface PartitionedAssignees { + usersWithProfiles: UserProfileWithAvatar[]; + usersWithoutProfiles: Assignee[]; +} + +export const useAssignees = ({ + caseAssignees, + userProfiles, + currentUserProfile, +}: { + caseAssignees: CaseAssignees; + userProfiles: Map<string, UserProfileWithAvatar>; + currentUserProfile: CurrentUserProfile; +}): { + assigneesWithProfiles: AssigneeWithProfile[]; + assigneesWithoutProfiles: Assignee[]; + allAssignees: Assignee[]; +} => { + const { assigneesWithProfiles, assigneesWithoutProfiles } = useMemo(() => { + const { usersWithProfiles, usersWithoutProfiles } = caseAssignees.reduce<PartitionedAssignees>( + (acc, assignee) => { + const profile = userProfiles.get(assignee.uid); + + if (profile) { + acc.usersWithProfiles.push(profile); + } else { + acc.usersWithoutProfiles.push({ uid: assignee.uid }); + } + + return acc; + }, + { usersWithProfiles: [], usersWithoutProfiles: [] } + ); + + const orderedProf = bringCurrentUserToFrontAndSort(currentUserProfile, usersWithProfiles); + + const assigneesWithProfile2 = orderedProf?.map((profile) => ({ uid: profile.uid, profile })); + return { + assigneesWithProfiles: assigneesWithProfile2 ?? [], + assigneesWithoutProfiles: usersWithoutProfiles, + }; + }, [caseAssignees, currentUserProfile, userProfiles]); + + const allAssignees = useMemo( + () => [...assigneesWithProfiles, ...assigneesWithoutProfiles], + [assigneesWithProfiles, assigneesWithoutProfiles] + ); + + return { + assigneesWithProfiles, + assigneesWithoutProfiles, + allAssignees, + }; +}; diff --git a/x-pack/plugins/cases/public/containers/user_profiles/use_bulk_get_user_profiles.test.ts b/x-pack/plugins/cases/public/containers/user_profiles/use_bulk_get_user_profiles.test.ts index 7591bf394d5c13..af0482f41b25ac 100644 --- a/x-pack/plugins/cases/public/containers/user_profiles/use_bulk_get_user_profiles.test.ts +++ b/x-pack/plugins/cases/public/containers/user_profiles/use_bulk_get_user_profiles.test.ts @@ -6,15 +6,18 @@ */ import { renderHook } from '@testing-library/react-hooks'; -import { useToasts } from '../../common/lib/kibana'; +import { useToasts, useKibana } from '../../common/lib/kibana'; import { AppMockRenderer, createAppMockRenderer } from '../../common/mock'; import * as api from './api'; import { useBulkGetUserProfiles } from './use_bulk_get_user_profiles'; import { userProfilesIds } from './api.mock'; +import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock'; jest.mock('../../common/lib/kibana'); jest.mock('./api'); +const useKibanaMock = useKibana as jest.Mock; + describe('useBulkGetUserProfiles', () => { const props = { uids: userProfilesIds, @@ -28,10 +31,13 @@ describe('useBulkGetUserProfiles', () => { beforeEach(() => { appMockRender = createAppMockRenderer(); jest.clearAllMocks(); + useKibanaMock.mockReturnValue({ + services: { ...createStartServicesMock() }, + }); }); it('calls bulkGetUserProfiles with correct arguments', async () => { - const spyOnSuggestUserProfiles = jest.spyOn(api, 'bulkGetUserProfiles'); + const spyOnBulkGetUserProfiles = jest.spyOn(api, 'bulkGetUserProfiles'); const { result, waitFor } = renderHook(() => useBulkGetUserProfiles(props), { wrapper: appMockRender.AppWrapper, @@ -39,16 +45,59 @@ describe('useBulkGetUserProfiles', () => { await waitFor(() => result.current.isSuccess); - expect(spyOnSuggestUserProfiles).toBeCalledWith({ + expect(spyOnBulkGetUserProfiles).toBeCalledWith({ ...props, security: expect.anything(), }); }); + it('returns a mapping with user profiles', async () => { + const { result, waitFor } = renderHook(() => useBulkGetUserProfiles(props), { + wrapper: appMockRender.AppWrapper, + }); + + await waitFor(() => result.current.isSuccess); + + expect(result.current.data).toMatchInlineSnapshot(` + Map { + "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0" => Object { + "data": Object {}, + "enabled": true, + "uid": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0", + "user": Object { + "email": "damaged_raccoon@elastic.co", + "full_name": "Damaged Raccoon", + "username": "damaged_raccoon", + }, + }, + "u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0" => Object { + "data": Object {}, + "enabled": true, + "uid": "u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0", + "user": Object { + "email": "physical_dinosaur@elastic.co", + "full_name": "Physical Dinosaur", + "username": "physical_dinosaur", + }, + }, + "u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0" => Object { + "data": Object {}, + "enabled": true, + "uid": "u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0", + "user": Object { + "email": "wet_dingo@elastic.co", + "full_name": "Wet Dingo", + "username": "wet_dingo", + }, + }, + } + `); + }); + it('shows a toast error message when an error occurs in the response', async () => { - const spyOnSuggestUserProfiles = jest.spyOn(api, 'bulkGetUserProfiles'); + const spyOnBulkGetUserProfiles = jest.spyOn(api, 'bulkGetUserProfiles'); - spyOnSuggestUserProfiles.mockImplementation(() => { + spyOnBulkGetUserProfiles.mockImplementation(() => { throw new Error('Something went wrong'); }); diff --git a/x-pack/plugins/cases/public/containers/user_profiles/use_bulk_get_user_profiles.ts b/x-pack/plugins/cases/public/containers/user_profiles/use_bulk_get_user_profiles.ts index 78c310462f77e2..de180b5970f3b7 100644 --- a/x-pack/plugins/cases/public/containers/user_profiles/use_bulk_get_user_profiles.ts +++ b/x-pack/plugins/cases/public/containers/user_profiles/use_bulk_get_user_profiles.ts @@ -6,24 +6,33 @@ */ import { useQuery, UseQueryResult } from '@tanstack/react-query'; -import { UserProfile } from '@kbn/security-plugin/common'; +import { UserProfileWithAvatar } from '@kbn/user-profile-components'; import * as i18n from '../translations'; import { useKibana, useToasts } from '../../common/lib/kibana'; import { ServerError } from '../../types'; import { USER_PROFILES_CACHE_KEY, USER_PROFILES_BULK_GET_CACHE_KEY } from '../constants'; import { bulkGetUserProfiles } from './api'; +const profilesToMap = (profiles: UserProfileWithAvatar[]): Map<string, UserProfileWithAvatar> => + profiles.reduce<Map<string, UserProfileWithAvatar>>((acc, profile) => { + acc.set(profile.uid, profile); + return acc; + }, new Map<string, UserProfileWithAvatar>()); + export const useBulkGetUserProfiles = ({ uids }: { uids: string[] }) => { const { security } = useKibana().services; const toasts = useToasts(); - return useQuery<UserProfile[], ServerError>( + return useQuery<UserProfileWithAvatar[], ServerError, Map<string, UserProfileWithAvatar>>( [USER_PROFILES_CACHE_KEY, USER_PROFILES_BULK_GET_CACHE_KEY, uids], () => { return bulkGetUserProfiles({ security, uids }); }, { + select: profilesToMap, + retry: false, + keepPreviousData: true, onError: (error: ServerError) => { if (error.name !== 'AbortError') { toasts.addError( @@ -38,4 +47,7 @@ export const useBulkGetUserProfiles = ({ uids }: { uids: string[] }) => { ); }; -export type UseSuggestUserProfiles = UseQueryResult<UserProfile[], ServerError>; +export type UseBulkGetUserProfiles = UseQueryResult< + Map<string, UserProfileWithAvatar>, + ServerError +>; diff --git a/x-pack/plugins/cases/public/containers/user_profiles/use_get_current_user_profile.test.ts b/x-pack/plugins/cases/public/containers/user_profiles/use_get_current_user_profile.test.ts new file mode 100644 index 00000000000000..ebc896a480cb04 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/user_profiles/use_get_current_user_profile.test.ts @@ -0,0 +1,97 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { useToasts, useKibana } from '../../common/lib/kibana'; +import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock'; +import { AppMockRenderer, createAppMockRenderer } from '../../common/mock'; +import * as api from './api'; +import { useGetCurrentUserProfile } from './use_get_current_user_profile'; + +jest.mock('../../common/lib/kibana'); +jest.mock('./api'); + +const useKibanaMock = useKibana as jest.Mock; + +describe('useGetCurrentUserProfile', () => { + const addSuccess = jest.fn(); + (useToasts as jest.Mock).mockReturnValue({ addSuccess, addError: jest.fn() }); + + let appMockRender: AppMockRenderer; + + beforeEach(() => { + appMockRender = createAppMockRenderer(); + jest.clearAllMocks(); + useKibanaMock.mockReturnValue({ + services: { ...createStartServicesMock() }, + }); + }); + + it('calls getCurrentUserProfile with correct arguments', async () => { + const spyOnGetCurrentUserProfile = jest.spyOn(api, 'getCurrentUserProfile'); + + const { result, waitFor } = renderHook(() => useGetCurrentUserProfile(), { + wrapper: appMockRender.AppWrapper, + }); + + await waitFor(() => result.current.isSuccess); + + expect(spyOnGetCurrentUserProfile).toBeCalledWith({ + security: expect.anything(), + }); + }); + + it('shows a toast error message when an error occurs in the response', async () => { + const spyOnGetCurrentUserProfile = jest.spyOn(api, 'getCurrentUserProfile'); + + spyOnGetCurrentUserProfile.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + const addError = jest.fn(); + (useToasts as jest.Mock).mockReturnValue({ addSuccess, addError }); + + const { result, waitFor } = renderHook(() => useGetCurrentUserProfile(), { + wrapper: appMockRender.AppWrapper, + }); + + await waitFor(() => result.current.isError); + + expect(addError).toHaveBeenCalled(); + }); + + it('does not show a toast error message when a 404 error is returned', async () => { + const spyOnGetCurrentUserProfile = jest.spyOn(api, 'getCurrentUserProfile'); + + spyOnGetCurrentUserProfile.mockImplementation(() => { + throw new MockServerError('profile not found', 404); + }); + + const addError = jest.fn(); + (useToasts as jest.Mock).mockReturnValue({ addSuccess, addError }); + + const { result, waitFor } = renderHook(() => useGetCurrentUserProfile(), { + wrapper: appMockRender.AppWrapper, + }); + + await waitFor(() => result.current.isError); + + expect(addError).not.toHaveBeenCalled(); + }); +}); + +class MockServerError extends Error { + public readonly body: { + statusCode: number; + }; + + constructor(message?: string, statusCode: number = 200) { + super(message); + this.name = this.constructor.name; + this.body = { statusCode }; + } +} diff --git a/x-pack/plugins/cases/public/containers/user_profiles/use_get_current_user_profile.ts b/x-pack/plugins/cases/public/containers/user_profiles/use_get_current_user_profile.ts new file mode 100644 index 00000000000000..37c29fa0b2d01c --- /dev/null +++ b/x-pack/plugins/cases/public/containers/user_profiles/use_get_current_user_profile.ts @@ -0,0 +1,44 @@ +/* + * 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 { useQuery, UseQueryResult } from '@tanstack/react-query'; +import { UserProfile } from '@kbn/security-plugin/common'; +import * as i18n from '../translations'; +import { useKibana, useToasts } from '../../common/lib/kibana'; +import { ServerError } from '../../types'; +import { USER_PROFILES_CACHE_KEY, USER_PROFILES_GET_CURRENT_CACHE_KEY } from '../constants'; +import { getCurrentUserProfile } from './api'; + +export const useGetCurrentUserProfile = () => { + const { security } = useKibana().services; + + const toasts = useToasts(); + + return useQuery<UserProfile, ServerError>( + [USER_PROFILES_CACHE_KEY, USER_PROFILES_GET_CURRENT_CACHE_KEY], + () => { + return getCurrentUserProfile({ security }); + }, + { + retry: false, + onError: (error: ServerError) => { + // Anonymous users (users authenticated via a proxy or configured in the kibana config) will result in a 404 + // from the security plugin. If this happens we'll silence the error and operate without the current user profile + if (error.name !== 'AbortError' && error.body?.statusCode !== 404) { + toasts.addError( + error.body && error.body.message ? new Error(error.body.message) : error, + { + title: i18n.ERROR_TITLE, + } + ); + } + }, + } + ); +}; + +export type UseGetCurrentUserProfile = UseQueryResult<UserProfile, ServerError>; diff --git a/x-pack/plugins/cases/public/containers/user_profiles/use_suggest_user_profiles.test.ts b/x-pack/plugins/cases/public/containers/user_profiles/use_suggest_user_profiles.test.ts index ef5fe32a23dff8..2d4482b94a9c64 100644 --- a/x-pack/plugins/cases/public/containers/user_profiles/use_suggest_user_profiles.test.ts +++ b/x-pack/plugins/cases/public/containers/user_profiles/use_suggest_user_profiles.test.ts @@ -18,7 +18,7 @@ jest.mock('./api'); describe('useSuggestUserProfiles', () => { const props = { name: 'elastic', - owner: [GENERAL_CASES_OWNER], + owners: [GENERAL_CASES_OWNER], }; const addSuccess = jest.fn(); diff --git a/x-pack/plugins/cases/public/containers/user_profiles/use_suggest_user_profiles.ts b/x-pack/plugins/cases/public/containers/user_profiles/use_suggest_user_profiles.ts index 6c83f853b2624e..26e03d0163c8e7 100644 --- a/x-pack/plugins/cases/public/containers/user_profiles/use_suggest_user_profiles.ts +++ b/x-pack/plugins/cases/public/containers/user_profiles/use_suggest_user_profiles.ts @@ -9,24 +9,42 @@ import { useState } from 'react'; import { useQuery, UseQueryResult } from '@tanstack/react-query'; import useDebounce from 'react-use/lib/useDebounce'; import { UserProfile } from '@kbn/security-plugin/common'; -import { DEFAULT_USER_SIZE } from '../../../common/constants'; +import { noop } from 'lodash'; +import { DEFAULT_USER_SIZE, SEARCH_DEBOUNCE_MS } from '../../../common/constants'; import * as i18n from '../translations'; import { useKibana, useToasts } from '../../common/lib/kibana'; import { ServerError } from '../../types'; import { USER_PROFILES_CACHE_KEY, USER_PROFILES_SUGGEST_CACHE_KEY } from '../constants'; import { suggestUserProfiles, SuggestUserProfilesArgs } from './api'; -const DEBOUNCE_MS = 500; +type Props = Omit<SuggestUserProfilesArgs, 'signal' | 'http'> & { onDebounce?: () => void }; + +/** + * Time in ms until the data become stale. + * We set the stale time to one minute + * to prevent fetching the same queries + * while the user is typing. + */ + +const STALE_TIME = 1000 * 60; export const useSuggestUserProfiles = ({ name, - owner, + owners, size = DEFAULT_USER_SIZE, -}: Omit<SuggestUserProfilesArgs, 'signal' | 'http'>) => { + onDebounce = noop, +}: Props) => { const { http } = useKibana().services; const [debouncedName, setDebouncedName] = useState(name); - useDebounce(() => setDebouncedName(name), DEBOUNCE_MS, [name]); + useDebounce( + () => { + setDebouncedName(name); + onDebounce(); + }, + SEARCH_DEBOUNCE_MS, + [name] + ); const toasts = useToasts(); @@ -34,20 +52,22 @@ export const useSuggestUserProfiles = ({ [ USER_PROFILES_CACHE_KEY, USER_PROFILES_SUGGEST_CACHE_KEY, - { name: debouncedName, owner, size }, + { name: debouncedName, owners, size }, ], () => { const abortCtrlRef = new AbortController(); return suggestUserProfiles({ http, name: debouncedName, - owner, + owners, size, signal: abortCtrlRef.signal, }); }, { retry: false, + keepPreviousData: true, + staleTime: STALE_TIME, onError: (error: ServerError) => { if (error.name !== 'AbortError') { toasts.addError( diff --git a/x-pack/plugins/cases/public/utils/permissions.test.ts b/x-pack/plugins/cases/public/utils/permissions.test.ts new file mode 100644 index 00000000000000..66e63e6950dd4c --- /dev/null +++ b/x-pack/plugins/cases/public/utils/permissions.test.ts @@ -0,0 +1,37 @@ +/* + * 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 { noCasesPermissions, readCasesPermissions } from '../common/mock'; +import { getAllPermissionsExceptFrom, isReadOnlyPermissions } from './permissions'; + +describe('permissions', () => { + describe('isReadOnlyPermissions', () => { + const tests = [['update'], ['create'], ['delete'], ['push'], ['all']]; + + it('returns true if the user has only read permissions', async () => { + expect(isReadOnlyPermissions(readCasesPermissions())).toBe(true); + }); + + it('returns true if the user has not read permissions', async () => { + expect(isReadOnlyPermissions(noCasesPermissions())).toBe(false); + }); + + it.each(tests)( + 'returns false if the user has permission %s=true and read=true', + async (permission) => { + const noPermissions = noCasesPermissions(); + expect(isReadOnlyPermissions({ ...noPermissions, [permission]: true })).toBe(false); + } + ); + }); + + describe('getAllPermissionsExceptFrom', () => { + it('returns the correct permissions', async () => { + expect(getAllPermissionsExceptFrom('create')).toEqual(['read', 'update', 'delete', 'push']); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/utils/permissions.ts b/x-pack/plugins/cases/public/utils/permissions.ts index 827535d484588a..75e15f8859e580 100644 --- a/x-pack/plugins/cases/public/utils/permissions.ts +++ b/x-pack/plugins/cases/public/utils/permissions.ts @@ -17,3 +17,10 @@ export const isReadOnlyPermissions = (permissions: CasesPermissions) => { permissions.read ); }; + +type CasePermission = Exclude<keyof CasesPermissions, 'all'>; + +export const allCasePermissions: CasePermission[] = ['create', 'read', 'update', 'delete', 'push']; + +export const getAllPermissionsExceptFrom = (capToExclude: CasePermission): CasePermission[] => + allCasePermissions.filter((permission) => permission !== capToExclude) as CasePermission[]; diff --git a/x-pack/plugins/cases/server/features.ts b/x-pack/plugins/cases/server/features.ts index d2ddc6a1030a04..9f92c7d9398a6b 100644 --- a/x-pack/plugins/cases/server/features.ts +++ b/x-pack/plugins/cases/server/features.ts @@ -55,7 +55,7 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => { ui: capabilities.all, }, read: { - api: ['bulkGetUserProfiles'], + api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles'], cases: { read: [APP_ID], }, diff --git a/x-pack/plugins/cases/server/services/user_profiles/index.ts b/x-pack/plugins/cases/server/services/user_profiles/index.ts index 36bc0c439a79e5..4ba9eb630e53f1 100644 --- a/x-pack/plugins/cases/server/services/user_profiles/index.ts +++ b/x-pack/plugins/cases/server/services/user_profiles/index.ts @@ -19,8 +19,8 @@ import { excess, SuggestUserProfilesRequestRt, throwErrors } from '../../../comm import { Operations } from '../../authorization'; import { createCaseError } from '../../common/error'; -const MAX_SUGGESTION_SIZE = 100; -const MIN_SUGGESTION_SIZE = 0; +const MAX_PROFILES_SIZE = 100; +const MIN_PROFILES_SIZE = 0; interface UserProfileOptions { securityPluginSetup?: SecurityPluginSetup; @@ -41,6 +41,32 @@ export class UserProfileService { this.options = options; } + private static suggestUsers({ + securityPluginStart, + spaceId, + searchTerm, + size, + owners, + }: { + securityPluginStart: SecurityPluginStart; + spaceId: string; + searchTerm: string; + size?: number; + owners: string[]; + }) { + return securityPluginStart.userProfiles.suggest({ + name: searchTerm, + size, + dataPath: 'avatar', + requiredPrivileges: { + spaceId, + privileges: { + kibana: UserProfileService.buildRequiredPrivileges(owners, securityPluginStart), + }, + }, + }); + } + public async suggest(request: KibanaRequest): Promise<UserProfile[]> { const params = pipe( excess(SuggestUserProfilesRequestRt).decode(request.body), @@ -61,29 +87,20 @@ export class UserProfileService { securityPluginStart: this.options.securityPluginStart, }; - /** - * The limit of 100 helps prevent DDoS attacks and is also enforced by the security plugin. - */ - if (size !== undefined && (size > MAX_SUGGESTION_SIZE || size < MIN_SUGGESTION_SIZE)) { - throw Boom.badRequest('size must be between 0 and 100'); - } + UserProfileService.validateSizeParam(size); - if (!UserProfileService.isSecurityEnabled(securityPluginFields)) { + if (!UserProfileService.isSecurityEnabled(securityPluginFields) || owners.length <= 0) { return []; } const { securityPluginStart } = securityPluginFields; - return securityPluginStart.userProfiles.suggest({ - name, + return UserProfileService.suggestUsers({ + searchTerm: name, size, - dataPath: 'avatar', - requiredPrivileges: { - spaceId: spaces.spacesService.getSpaceId(request), - privileges: { - kibana: UserProfileService.buildRequiredPrivileges(owners, securityPluginStart), - }, - }, + owners, + securityPluginStart, + spaceId: spaces.spacesService.getSpaceId(request), }); } catch (error) { throw createCaseError({ @@ -94,6 +111,15 @@ export class UserProfileService { } } + private static validateSizeParam(size?: number) { + /** + * The limit of 100 helps prevent DDoS attacks and is also enforced by the security plugin. + */ + if (size !== undefined && (size > MAX_PROFILES_SIZE || size < MIN_PROFILES_SIZE)) { + throw Boom.badRequest('size must be between 0 and 100'); + } + } + private static isSecurityEnabled(fields: { securityPluginSetup?: SecurityPluginSetup; securityPluginStart?: SecurityPluginStart; diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/eks_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/eks_form.tsx index 2de87bdb660f7c..7cf3fb779942cb 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/eks_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/eks_form.tsx @@ -23,14 +23,35 @@ export const eksVars = [ id: 'secret_access_key', label: i18n.translate( 'xpack.csp.createPackagePolicy.eksIntegrationSettingsSection.secretAccessKeyFieldLabel', - { defaultMessage: 'Secret access key' } + { defaultMessage: 'Secret Access Key' } ), }, { id: 'session_token', label: i18n.translate( 'xpack.csp.createPackagePolicy.eksIntegrationSettingsSection.sessionTokenFieldLabel', - { defaultMessage: 'Session token' } + { defaultMessage: 'Session Token' } + ), + }, + { + id: 'shared_credential_file', + label: i18n.translate( + 'xpack.csp.createPackagePolicy.eksIntegrationSettingsSection.sharedCredentialsFileFieldLabel', + { defaultMessage: 'Shared Credential File' } + ), + }, + { + id: 'credential_profile_name', + label: i18n.translate( + 'xpack.csp.createPackagePolicy.eksIntegrationSettingsSection.sharedCredentialFileFieldLabel', + { defaultMessage: 'Credential Profile Name' } + ), + }, + { + id: 'role_arn', + label: i18n.translate( + 'xpack.csp.createPackagePolicy.eksIntegrationSettingsSection.roleARNFieldLabel', + { defaultMessage: 'ARN Role' } ), }, ] as const; @@ -50,6 +71,9 @@ const getEksVars = (input?: NewPackagePolicyInput): EksFormVars => { access_key_id: vars?.access_key_id.value || '', secret_access_key: vars?.secret_access_key.value || '', session_token: vars?.session_token.value || '', + shared_credential_file: vars?.shared_credential_file.value || '', + credential_profile_name: vars?.credential_profile_name.value || '', + role_arn: vars?.role_arn.value || '', }; }; diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/mocks.ts b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/mocks.ts index 2af55809f1c913..05be275af41c04 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/mocks.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/mocks.ts @@ -51,6 +51,15 @@ export const getCspNewPolicyMock = (type: BenchmarkId = 'cis_k8s'): NewPackagePo session_token: { type: 'text', }, + shared_credential_file: { + type: 'text', + }, + credential_profile_name: { + type: 'text', + }, + role_arn: { + type: 'text', + }, }, }, ], diff --git a/x-pack/plugins/cloud_security_posture/server/fleet_integration/fleet_integration.ts b/x-pack/plugins/cloud_security_posture/server/fleet_integration/fleet_integration.ts index 6c5701a6593ee2..39db06473c9a96 100644 --- a/x-pack/plugins/cloud_security_posture/server/fleet_integration/fleet_integration.ts +++ b/x-pack/plugins/cloud_security_posture/server/fleet_integration/fleet_integration.ts @@ -17,6 +17,7 @@ import { DeletePackagePoliciesResponse, PackagePolicyInput, } from '@kbn/fleet-plugin/common'; +import { DeepReadonly } from 'utility-types'; import { createCspRuleSearchFilterByPackagePolicy } from '../../common/utils/helpers'; import { CLOUDBEAT_VANILLA, @@ -85,7 +86,7 @@ export const onPackagePolicyPostCreateCallback = async ( * Callback to handle deletion of PackagePolicies in Fleet */ export const removeCspRulesInstancesCallback = async ( - deletedPackagePolicy: DeletePackagePoliciesResponse[number], + deletedPackagePolicy: DeepReadonly<DeletePackagePoliciesResponse[number]>, soClient: ISavedObjectsRepository, logger: Logger ): Promise<void> => { diff --git a/x-pack/plugins/embeddable_enhanced/public/plugin.ts b/x-pack/plugins/embeddable_enhanced/public/plugin.ts index 61958c114c632c..ed829a9326e9e3 100644 --- a/x-pack/plugins/embeddable_enhanced/public/plugin.ts +++ b/x-pack/plugins/embeddable_enhanced/public/plugin.ts @@ -6,7 +6,6 @@ */ import { CoreStart, CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/public'; -import { SavedObjectAttributes } from '@kbn/core/public'; import { EmbeddableFactory, EmbeddableFactoryDefinition, @@ -77,7 +76,7 @@ export class EmbeddableEnhancedPlugin I extends EmbeddableInput = EmbeddableInput, O extends EmbeddableOutput = EmbeddableOutput, E extends IEmbeddable<I, O> = IEmbeddable<I, O>, - T extends SavedObjectAttributes = SavedObjectAttributes + T = unknown >( def: EmbeddableFactoryDefinition<I, O, E, T> ): EmbeddableFactory<I, O, E, T> => { diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts index 970f3baed7ab1a..527e4bcd82535b 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { coreMock } from '@kbn/core/server/mocks'; +import { coreMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { securityMock } from '@kbn/security-plugin/server/mocks'; import { ConfigSchema } from './config'; @@ -28,11 +28,12 @@ describe('EncryptedSavedObjects Plugin', () => { }); it('exposes proper contract when encryption key is set', () => { - const plugin = new EncryptedSavedObjectsPlugin( - coreMock.createPluginInitializerContext( - ConfigSchema.validate({ encryptionKey: 'z'.repeat(32) }, { dist: true }) - ) + const mockInitializerContext = coreMock.createPluginInitializerContext( + ConfigSchema.validate({ encryptionKey: 'z'.repeat(32) }, { dist: true }) ); + + const plugin = new EncryptedSavedObjectsPlugin(mockInitializerContext); + expect(plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() })) .toMatchInlineSnapshot(` Object { @@ -41,6 +42,13 @@ describe('EncryptedSavedObjects Plugin', () => { "registerType": [Function], } `); + + const infoLogs = loggingSystemMock.collect(mockInitializerContext.logger).info; + + expect(infoLogs.length).toBe(1); + expect(infoLogs[0]).toEqual([ + `Hashed 'xpack.encryptedSavedObjects.encryptionKey' for this instance: WLbjNGKEm7aA4NfJHYyW88jHUkHtyF7ENHcF0obYGBU=`, + ]); }); }); diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index 5ba387128ef7e0..92a663fab64d40 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -6,6 +6,7 @@ */ import nodeCrypto from '@elastic/node-crypto'; +import { createHash } from 'crypto'; import type { CoreSetup, Logger, Plugin, PluginInitializerContext } from '@kbn/core/server'; import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; @@ -63,6 +64,14 @@ export class EncryptedSavedObjectsPlugin 'Saved objects encryption key is not set. This will severely limit Kibana functionality. ' + 'Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' ); + } else { + const hashedEncryptionKey = createHash('sha3-256') + .update(config.encryptionKey) + .digest('base64'); + + this.logger.info( + `Hashed 'xpack.encryptedSavedObjects.encryptionKey' for this instance: ${hashedEncryptionKey}` + ); } const primaryCrypto = config.encryptionKey diff --git a/x-pack/plugins/enterprise_search/common/types/connectors.ts b/x-pack/plugins/enterprise_search/common/types/connectors.ts index 26b7ef917f4353..2f5b47c824c9df 100644 --- a/x-pack/plugins/enterprise_search/common/types/connectors.ts +++ b/x-pack/plugins/enterprise_search/common/types/connectors.ts @@ -30,6 +30,14 @@ export enum SyncStatus { COMPLETED = 'completed', ERROR = 'error', } + +export interface IngestPipelineParams { + extract_binary_content: boolean; + name: string; + reduce_whitespace: boolean; + run_ml_inference: boolean; +} + export interface Connector { api_key_id: string | null; configuration: ConnectorConfiguration; @@ -42,6 +50,7 @@ export interface Connector { last_sync_status: SyncStatus | null; last_synced: string | null; name: string; + pipeline?: IngestPipelineParams | null; scheduling: { enabled: boolean; interval: string; // crontab syntax diff --git a/x-pack/plugins/enterprise_search/common/types/indices.ts b/x-pack/plugins/enterprise_search/common/types/indices.ts index 78831e16150046..d047ec9ba36d74 100644 --- a/x-pack/plugins/enterprise_search/common/types/indices.ts +++ b/x-pack/plugins/enterprise_search/common/types/indices.ts @@ -36,13 +36,9 @@ export interface ElasticsearchIndex { export interface ConnectorIndex extends ElasticsearchIndex { connector: Connector; } -export interface ConnectorCrawlerIndex extends ElasticsearchIndex { - connector: Connector; - crawler: Crawler; -} export interface CrawlerIndex extends ElasticsearchIndex { - connector?: Connector; crawler: Crawler; + connector?: Connector; } export interface ElasticsearchIndexWithPrivileges extends ElasticsearchIndex { diff --git a/x-pack/plugins/enterprise_search/common/ui_settings_keys.ts b/x-pack/plugins/enterprise_search/common/ui_settings_keys.ts index bccb83e63e01bc..1007c3f4421af4 100644 --- a/x-pack/plugins/enterprise_search/common/ui_settings_keys.ts +++ b/x-pack/plugins/enterprise_search/common/ui_settings_keys.ts @@ -6,4 +6,4 @@ */ export const enterpriseSearchFeatureId = 'enterpriseSearch'; -export const enableIndexTransformsTab = 'enterpriseSearch:enableIndexTransformsTab'; +export const enableIndexPipelinesTab = 'enterpriseSearch:enableIndexTransformsTab'; diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/api/delete_analytics_collection/delete_analytics_collection_api_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/api/delete_analytics_collection/delete_analytics_collection_api_logic.test.ts new file mode 100644 index 00000000000000..1ad6c18fea3bf8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/api/delete_analytics_collection/delete_analytics_collection_api_logic.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { mockHttpValues } from '../../../__mocks__/kea_logic'; + +import { nextTick } from '@kbn/test-jest-helpers'; + +import { deleteAnalyticsCollection } from './delete_analytics_collection_api_logic'; + +describe('DeleteAnalyticsCollectionApiLogic', () => { + const { http } = mockHttpValues; + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('DeleteAnalyticsCollectionsApiLogic', () => { + it('calls the analytics collections list api', async () => { + const promise = Promise.resolve(); + const name = 'collection'; + http.delete.mockReturnValue(promise); + const result = deleteAnalyticsCollection({ name }); + await nextTick(); + expect(http.delete).toHaveBeenCalledWith( + `/internal/enterprise_search/analytics/collections/${name}` + ); + await expect(result).resolves.toEqual(undefined); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/api/delete_analytics_collection/delete_analytics_collection_api_logic.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/api/delete_analytics_collection/delete_analytics_collection_api_logic.tsx new file mode 100644 index 00000000000000..0b2f092899a6fb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/api/delete_analytics_collection/delete_analytics_collection_api_logic.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createApiLogic } from '../../../shared/api_logic/create_api_logic'; +import { HttpLogic } from '../../../shared/http'; + +export type DeleteAnalyticsCollectionApiLogicResponse = void; + +export const deleteAnalyticsCollection = async ({ name }: { name: string }) => { + const { http } = HttpLogic.values; + const route = `/internal/enterprise_search/analytics/collections/${name}`; + await http.delete<DeleteAnalyticsCollectionApiLogicResponse>(route); + + return; +}; + +export const DeleteAnalyticsCollectionAPILogic = createApiLogic( + ['analytics', 'delete_analytics_collection_api_logic'], + deleteAnalyticsCollection +); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/api/fetch_analytics_collection/fetch_analytics_collection_api_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/api/fetch_analytics_collection/fetch_analytics_collection_api_logic.test.ts new file mode 100644 index 00000000000000..ae21c61a8fad23 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/api/fetch_analytics_collection/fetch_analytics_collection_api_logic.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { mockHttpValues } from '../../../__mocks__/kea_logic'; + +import { nextTick } from '@kbn/test-jest-helpers'; + +import { fetchAnalyticsCollection } from './fetch_analytics_collection_api_logic'; + +describe('FetchAnalyticsCollectionApiLogic', () => { + const { http } = mockHttpValues; + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('FetchAnalyticsCollectionsApiLogic', () => { + it('calls the analytics collections list api', async () => { + const promise = Promise.resolve({ name: 'result' }); + const name = 'collection'; + http.get.mockReturnValue(promise); + const result = fetchAnalyticsCollection({ name }); + await nextTick(); + expect(http.get).toHaveBeenCalledWith( + `/internal/enterprise_search/analytics/collections/${name}` + ); + await expect(result).resolves.toEqual({ name: 'result' }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/api/fetch_analytics_collection/fetch_analytics_collection_api_logic.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/api/fetch_analytics_collection/fetch_analytics_collection_api_logic.tsx new file mode 100644 index 00000000000000..5aafc82c29e0ef --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/api/fetch_analytics_collection/fetch_analytics_collection_api_logic.tsx @@ -0,0 +1,26 @@ +/* + * 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 { AnalyticsCollection } from '../../../../../common/types/analytics'; + +import { createApiLogic } from '../../../shared/api_logic/create_api_logic'; +import { HttpLogic } from '../../../shared/http'; + +export type FetchAnalyticsCollectionApiLogicResponse = AnalyticsCollection; + +export const fetchAnalyticsCollection = async ({ name }: { name: string }) => { + const { http } = HttpLogic.values; + const route = `/internal/enterprise_search/analytics/collections/${name}`; + const response = await http.get<AnalyticsCollection>(route); + + return response; +}; + +export const FetchAnalyticsCollectionAPILogic = createApiLogic( + ['analytics', 'analytics_collection_api_logic'], + fetchAnalyticsCollection +); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/add_analytics_collections/add_analytics_collection_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/components/add_analytics_collections/add_analytics_collection_logic.test.ts index b9355574e0806e..7b281208f79e8b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/add_analytics_collections/add_analytics_collection_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/add_analytics_collections/add_analytics_collection_logic.test.ts @@ -110,7 +110,7 @@ describe('addAnalyticsCollectionLogic', () => { expect(flashSuccessToast).toHaveBeenCalled(); jest.advanceTimersByTime(1000); await nextTick(); - expect(navigateToUrl).toHaveBeenCalledWith('/collections/test'); + expect(navigateToUrl).toHaveBeenCalledWith('/collections/test/events'); jest.useRealTimers(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/add_analytics_collections/add_analytics_collection_logic.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/components/add_analytics_collections/add_analytics_collection_logic.ts index a1035168d8e87c..3731bb3994e9ef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/add_analytics_collections/add_analytics_collection_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/add_analytics_collections/add_analytics_collection_logic.ts @@ -78,6 +78,7 @@ export const AddAnalyticsCollectionLogic = kea< KibanaLogic.values.navigateToUrl( generateEncodedPath(COLLECTION_VIEW_PATH, { name, + section: 'events', }) ); }, diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate.test.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate.test.tsx new file mode 100644 index 00000000000000..f46454e45eb820 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../__mocks__/shallow_useeffect.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiCodeBlock } from '@elastic/eui'; + +import { AnalyticsCollection } from '../../../../../common/types/analytics'; + +import { AnalyticsCollectionIntegrate } from './analytics_collection_integrate'; + +describe('AnalyticsCollectionIntegrate', () => { + const analyticsCollections: AnalyticsCollection = { + event_retention_day_length: 180, + id: '1', + name: 'example', + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const wrapper = shallow(<AnalyticsCollectionIntegrate collection={analyticsCollections} />); + expect(wrapper.find(EuiCodeBlock)).toHaveLength(2); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate.tsx new file mode 100644 index 00000000000000..ff5cdf9c5fee17 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate.tsx @@ -0,0 +1,121 @@ +/* + * 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 from 'react'; + +import { + EuiCodeBlock, + EuiDescriptionList, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { AnalyticsCollection } from '../../../../../common/types/analytics'; +import { getEnterpriseSearchUrl } from '../../../shared/enterprise_search_url'; + +interface AnalyticsCollectionIntegrateProps { + collection: AnalyticsCollection; +} + +export const AnalyticsCollectionIntegrate: React.FC<AnalyticsCollectionIntegrateProps> = ({ + collection, +}) => { + const analyticsDNSUrl = getEnterpriseSearchUrl(`/analytics/${collection.name}`); + const credentials = [ + { + title: i18n.translate( + 'xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.credentials.collectionName', + { + defaultMessage: 'Collection name', + } + ), + description: collection.name, + }, + { + title: i18n.translate( + 'xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.credentials.collectionDns', + { + defaultMessage: 'DNS URL', + } + ), + description: analyticsDNSUrl, + }, + ]; + const webclientSrc = getEnterpriseSearchUrl('/analytics.js'); + + return ( + <> + <EuiTitle> + <h4> + {i18n.translate( + 'xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.credentials.headingTitle', + { + defaultMessage: 'Credentials', + } + )} + </h4> + </EuiTitle> + <EuiSpacer size="s" /> + <EuiPanel hasShadow={false} color="subdued" paddingSize="xl" grow={false}> + <EuiDescriptionList listItems={credentials} type="column" align="center" /> + </EuiPanel> + + <EuiSpacer size="l" /> + + <EuiTitle> + <h4> + {i18n.translate( + 'xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.embed.headingTitle', + { + defaultMessage: 'Start tracking events', + } + )} + </h4> + </EuiTitle> + <EuiSpacer size="s" /> + <EuiText size="s"> + <p> + {i18n.translate( + 'xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.embed.description', + { + defaultMessage: + 'Embed the JS snippet below on every page of the website or application you’d like to tracks.', + } + )} + </p> + </EuiText> + <EuiSpacer size="s" /> + <EuiCodeBlock language="html" isCopyable> + {`<script src="${webclientSrc}" data-dsn="${analyticsDNSUrl}" defer></script>`} + </EuiCodeBlock> + + <EuiSpacer size="l" /> + <EuiText size="s"> + <p> + {i18n.translate( + 'xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.scriptDescription', + { + defaultMessage: + 'Track individual events, like clicks, by calling the <strong>trackEvent</strong> method.', + } + )} + </p> + </EuiText> + <EuiSpacer size="s" /> + <EuiCodeBlock language="js" isCopyable> + {`window.elasticAnalytics.trackEvent("ResultClick", { + title: "Website Analytics", + url: "www.elasitc.co/analytics/website" +})`} + </EuiCodeBlock> + </> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_settings.test.tsx new file mode 100644 index 00000000000000..c2d4374c5a4f6d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_settings.test.tsx @@ -0,0 +1,51 @@ +/* + * 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 { setMockActions } from '../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButton, EuiPanel } from '@elastic/eui'; + +import { AnalyticsCollection } from '../../../../../common/types/analytics'; + +import { AnalyticsCollectionSettings } from './analytics_collection_settings'; + +const mockActions = { + deleteAnalyticsCollection: jest.fn(), + setNameValue: jest.fn(), +}; + +const analyticsCollection: AnalyticsCollection = { + event_retention_day_length: 180, + id: '1', + name: 'example', +}; + +describe('AnalyticsCollectionSettings', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + setMockActions(mockActions); + + const wrapper = shallow(<AnalyticsCollectionSettings collection={analyticsCollection} />); + expect(wrapper.find(EuiPanel)).toHaveLength(2); + }); + + it('deletes analytics collection when delete is clicked', () => { + setMockActions(mockActions); + + const wrapper = shallow(<AnalyticsCollectionSettings collection={analyticsCollection} />); + + wrapper.find(EuiButton).simulate('click', { preventDefault: jest.fn() }); + expect(mockActions.deleteAnalyticsCollection).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_settings.tsx new file mode 100644 index 00000000000000..ac3a2fe490e404 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_settings.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, + EuiButton, + EuiDescriptionList, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { AnalyticsCollection } from '../../../../../common/types/analytics'; + +import { DeleteAnalyticsCollectionLogic } from './delete_analytics_collection_logic'; + +interface AnalyticsCollectionSettingsProps { + collection: AnalyticsCollection; +} + +export const AnalyticsCollectionSettings: React.FC<AnalyticsCollectionSettingsProps> = ({ + collection, +}) => { + const { deleteAnalyticsCollection } = useActions(DeleteAnalyticsCollectionLogic); + const { isLoading } = useValues(DeleteAnalyticsCollectionLogic); + + return ( + <> + <EuiPanel hasShadow={false} color="subdued" paddingSize="xl" grow={false}> + <EuiDescriptionList + listItems={[ + { + title: i18n.translate( + 'xpack.enterpriseSearch.analytics.collections.collectionsView.settingsTab.credentials.collectionName', + { + defaultMessage: 'Collection name', + } + ), + description: collection.name, + }, + ]} + type="column" + align="center" + /> + </EuiPanel> + <EuiSpacer size="l" /> + <EuiPanel hasShadow={false} color="danger" paddingSize="l"> + <EuiTitle size="s"> + <h4> + {i18n.translate( + 'xpack.enterpriseSearch.analytics.collections.collectionsView.settingsTab.delete.headingTitle', + { + defaultMessage: 'Delete this analytics collection', + } + )} + </h4> + </EuiTitle> + <EuiSpacer size="s" /> + <EuiText size="s"> + <p> + {i18n.translate( + 'xpack.enterpriseSearch.analytics.collections.collectionsView.settingsTab.delete.warning', + { + defaultMessage: 'This action is irreversible', + } + )} + </p> + </EuiText> + <EuiSpacer /> + <EuiButton + fill + type="submit" + color="danger" + isLoading={!isLoading} + disabled={!isLoading} + onClick={() => { + deleteAnalyticsCollection(collection.name); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.analytics.collections.collectionsView.settingsTab.delete.buttonTitle', + { + defaultMessage: 'Delete this collection', + } + )} + </EuiButton> + </EuiPanel> + </> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.test.tsx new file mode 100644 index 00000000000000..6fc82551818ce6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.test.tsx @@ -0,0 +1,68 @@ +/* + * 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 '../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic'; +import { mockUseParams } from '../../../__mocks__/react_router'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { AnalyticsCollection } from '../../../../../common/types/analytics'; + +import { AnalyticsCollectionIntegrate } from './analytics_collection_integrate'; +import { AnalyticsCollectionSettings } from './analytics_collection_settings'; + +import { AnalyticsCollectionView } from './analytics_collection_view'; + +const mockValues = { + analyticsCollection: { + event_retention_day_length: 180, + id: '1', + name: 'Analytics Collection 1', + } as AnalyticsCollection, +}; + +const mockActions = { + fetchAnalyticsCollection: jest.fn(), +}; + +describe('AnalyticsOverview', () => { + beforeEach(() => { + jest.clearAllMocks(); + + mockUseParams.mockReturnValue({ name: '1', section: 'settings' }); + }); + + describe('empty state', () => { + it('renders when analytics collection is empty on inital query', () => { + setMockValues({ + ...mockValues, + analyticsCollection: null, + }); + setMockActions(mockActions); + const wrapper = shallow(<AnalyticsCollectionView />); + + expect(mockActions.fetchAnalyticsCollection).toHaveBeenCalled(); + + expect(wrapper.find(AnalyticsCollectionSettings)).toHaveLength(0); + expect(wrapper.find(AnalyticsCollectionIntegrate)).toHaveLength(0); + }); + + it('renders with Data', async () => { + setMockValues(mockValues); + setMockActions(mockActions); + + const wrapper = shallow(<AnalyticsCollectionView />); + + expect(wrapper.find(AnalyticsCollectionSettings)).toHaveLength(1); + expect(mockActions.fetchAnalyticsCollection).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.tsx new file mode 100644 index 00000000000000..4c8e5a36e4e0f3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.tsx @@ -0,0 +1,189 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import { useParams } from 'react-router-dom'; + +import { useActions, useValues } from 'kea'; + +import { + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, + EuiIcon, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { generateEncodedPath } from '../../../shared/encode_path_params'; +import { KibanaLogic } from '../../../shared/kibana'; +import { EuiButtonTo } from '../../../shared/react_router_helpers'; +import { COLLECTION_CREATION_PATH, COLLECTION_VIEW_PATH } from '../../routes'; + +import { EnterpriseSearchAnalyticsPageTemplate } from '../layout/page_template'; + +import { AnalyticsCollectionIntegrate } from './analytics_collection_integrate'; +import { AnalyticsCollectionSettings } from './analytics_collection_settings'; + +import { FetchAnalyticsCollectionLogic } from './fetch_analytics_collection_logic'; + +export const collectionViewBreadcrumbs = [ + i18n.translate('xpack.enterpriseSearch.analytics.collectionsView.breadcrumb', { + defaultMessage: 'View collection', + }), +]; + +export const AnalyticsCollectionView: React.FC = () => { + const { fetchAnalyticsCollection } = useActions(FetchAnalyticsCollectionLogic); + const { analyticsCollection, isLoading } = useValues(FetchAnalyticsCollectionLogic); + const { name, section } = useParams<{ name: string; section: string }>(); + const { navigateToUrl } = useValues(KibanaLogic); + const collectionViewTabs = [ + { + id: 'events', + label: i18n.translate('xpack.enterpriseSearch.analytics.collectionsView.tabs.eventsName', { + defaultMessage: 'Events', + }), + onClick: () => + navigateToUrl( + generateEncodedPath(COLLECTION_VIEW_PATH, { + name: analyticsCollection?.name, + section: 'events', + }) + ), + isSelected: section === 'events', + }, + { + id: 'integrate', + label: i18n.translate('xpack.enterpriseSearch.analytics.collectionsView.tabs.integrateName', { + defaultMessage: 'Integrate', + }), + prepend: <EuiIcon type="editorCodeBlock" size="l" />, + onClick: () => + navigateToUrl( + generateEncodedPath(COLLECTION_VIEW_PATH, { + name: analyticsCollection?.name, + section: 'integrate', + }) + ), + isSelected: section === 'integrate', + }, + { + id: 'settings', + label: i18n.translate('xpack.enterpriseSearch.analytics.collectionsView.tabs.settingsName', { + defaultMessage: 'Settings', + }), + onClick: () => + navigateToUrl( + generateEncodedPath(COLLECTION_VIEW_PATH, { + name: analyticsCollection?.name, + section: 'settings', + }) + ), + isSelected: section === 'settings', + }, + ]; + + useEffect(() => { + fetchAnalyticsCollection(name); + }, []); + + return ( + <EnterpriseSearchAnalyticsPageTemplate + restrictWidth + isLoading={isLoading} + pageChrome={[...collectionViewBreadcrumbs]} + pageViewTelemetry="View Analytics Collection" + pageHeader={{ + description: i18n.translate( + 'xpack.enterpriseSearch.analytics.collectionsView.pageDescription', + { + defaultMessage: + 'Dashboards and tools for visualizing end-user behavior and measuring the performance of your search applications. Track trends over time, identify and investigate anomalies, and make optimizations.', + } + ), + pageTitle: analyticsCollection?.name, + tabs: [...collectionViewTabs], + }} + > + {!analyticsCollection && ( + <EuiFlexGroup> + <EuiFlexItem> + <EuiTitle> + <h2> + {i18n.translate( + 'xpack.enterpriseSearch.analytics.collections.collectionsView.headingTitle', + { + defaultMessage: 'Collections', + } + )} + </h2> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonTo fill iconType="plusInCircle" to={COLLECTION_CREATION_PATH}> + {i18n.translate( + 'xpack.enterpriseSearch.analytics.collections.collectionsView.create.buttonTitle', + { + defaultMessage: 'Create new collection', + } + )} + </EuiButtonTo> + </EuiFlexItem> + </EuiFlexGroup> + )} + + <EuiSpacer size="l" /> + {analyticsCollection ? ( + <> + {section === 'settings' && ( + <AnalyticsCollectionSettings collection={analyticsCollection} /> + )} + {section === 'integrate' && ( + <AnalyticsCollectionIntegrate collection={analyticsCollection} /> + )} + </> + ) : ( + <EuiEmptyPrompt + iconType="search" + title={ + <h2> + {i18n.translate( + 'xpack.enterpriseSearch.analytics.collections.collectionsView.collectionNotFoundState.headingTitle', + { + defaultMessage: 'You may have deleted this analytics collection', + } + )} + </h2> + } + body={ + <p> + {i18n.translate( + 'xpack.enterpriseSearch.analytics.collections.collectionsView.collectionNotFoundState.subHeading', + { + defaultMessage: + 'An analytics collection provides a place to store the analytics events for any given search application you are building. Create a new collection to get started.', + } + )} + </p> + } + actions={[ + <EuiButtonTo fill iconType="plusInCircle" to={COLLECTION_CREATION_PATH}> + {i18n.translate( + 'xpack.enterpriseSearch.analytics.collections.collectionsView.create.buttonTitle', + { + defaultMessage: 'Create new collection', + } + )} + </EuiButtonTo>, + ]} + /> + )} + </EnterpriseSearchAnalyticsPageTemplate> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/delete_analytics_collection_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/delete_analytics_collection_logic.test.ts new file mode 100644 index 00000000000000..08ca206671de02 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/delete_analytics_collection_logic.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + LogicMounter, + mockFlashMessageHelpers, + mockHttpValues, +} from '../../../__mocks__/kea_logic'; + +import { nextTick } from '@kbn/test-jest-helpers'; + +import { AnalyticsCollection } from '../../../../../common/types/analytics'; +import { HttpError, Status } from '../../../../../common/types/api'; + +import { DeleteAnalyticsCollectionLogic } from './delete_analytics_collection_logic'; + +describe('deleteAnalyticsCollectionLogic', () => { + const { mount } = new LogicMounter(DeleteAnalyticsCollectionLogic); + const { http } = mockHttpValues; + + beforeEach(() => { + jest.clearAllMocks(); + jest.useRealTimers(); + mount(); + }); + + const DEFAULT_VALUES = { + isLoading: true, + status: Status.IDLE, + }; + + it('has expected default values', () => { + expect(DeleteAnalyticsCollectionLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('listeners', () => { + it('calls clearFlashMessages on new makeRequest', async () => { + const promise = Promise.resolve(undefined); + http.delete.mockReturnValue(promise); + + await nextTick(); + + DeleteAnalyticsCollectionLogic.actions.makeRequest({ name: 'name' } as AnalyticsCollection); + expect(mockFlashMessageHelpers.clearFlashMessages).toHaveBeenCalledTimes(1); + }); + + it('calls flashAPIErrors on apiError', () => { + DeleteAnalyticsCollectionLogic.actions.apiError({} as HttpError); + expect(mockFlashMessageHelpers.flashAPIErrors).toHaveBeenCalledTimes(1); + expect(mockFlashMessageHelpers.flashAPIErrors).toHaveBeenCalledWith({}); + }); + + it('calls makeRequest on deleteAnalyticsCollections', async () => { + const collectionName = 'name'; + + jest.useFakeTimers(); + DeleteAnalyticsCollectionLogic.actions.makeRequest = jest.fn(); + DeleteAnalyticsCollectionLogic.actions.deleteAnalyticsCollection(collectionName); + jest.advanceTimersByTime(150); + await nextTick(); + expect(DeleteAnalyticsCollectionLogic.actions.makeRequest).toHaveBeenCalledWith({ + name: collectionName, + }); + }); + }); + + describe('selectors', () => { + describe('analyticsCollection', () => { + it('updates when apiSuccess listener triggered', () => { + DeleteAnalyticsCollectionLogic.actions.apiSuccess(); + + expect(DeleteAnalyticsCollectionLogic.values).toEqual({ + ...DEFAULT_VALUES, + isLoading: false, + status: Status.SUCCESS, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/delete_analytics_collection_logic.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/delete_analytics_collection_logic.ts new file mode 100644 index 00000000000000..0d6bf900f528ae --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/delete_analytics_collection_logic.ts @@ -0,0 +1,73 @@ +/* + * 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 { kea, MakeLogicType } from 'kea'; + +import { i18n } from '@kbn/i18n'; + +import { AnalyticsCollection } from '../../../../../common/types/analytics'; +import { Status } from '../../../../../common/types/api'; +import { Actions } from '../../../shared/api_logic/create_api_logic'; +import { + flashAPIErrors, + clearFlashMessages, + flashSuccessToast, +} from '../../../shared/flash_messages'; +import { KibanaLogic } from '../../../shared/kibana'; +import { + DeleteAnalyticsCollectionAPILogic, + DeleteAnalyticsCollectionApiLogicResponse, +} from '../../api/delete_analytics_collection/delete_analytics_collection_api_logic'; +import { ROOT_PATH } from '../../routes'; + +export interface DeleteAnalyticsCollectionActions { + apiError: Actions<{}, DeleteAnalyticsCollectionApiLogicResponse>['apiError']; + apiSuccess: Actions<{}, DeleteAnalyticsCollectionApiLogicResponse>['apiSuccess']; + deleteAnalyticsCollection(name: string): { name: string }; + makeRequest: Actions<{}, DeleteAnalyticsCollectionApiLogicResponse>['makeRequest']; +} +export interface DeleteAnalyticsCollectionValues { + analyticsCollection: AnalyticsCollection; + isLoading: boolean; + status: Status; +} + +export const DeleteAnalyticsCollectionLogic = kea< + MakeLogicType<DeleteAnalyticsCollectionValues, DeleteAnalyticsCollectionActions> +>({ + actions: { + deleteAnalyticsCollection: (name) => ({ name }), + }, + connect: { + actions: [DeleteAnalyticsCollectionAPILogic, ['makeRequest', 'apiSuccess', 'apiError']], + values: [DeleteAnalyticsCollectionAPILogic, ['status']], + }, + listeners: ({ actions }) => ({ + apiError: (e) => flashAPIErrors(e), + apiSuccess: async (undefined, breakpoint) => { + flashSuccessToast( + i18n.translate('xpack.enterpriseSearch.analytics.collectionsDelete.action.successMessage', { + defaultMessage: 'The collection has been successfully deleted', + }) + ); + // Wait for propagation of the collection deletion + await breakpoint(1000); + KibanaLogic.values.navigateToUrl(ROOT_PATH); + }, + deleteAnalyticsCollection: ({ name }) => { + actions.makeRequest({ name }); + }, + makeRequest: () => clearFlashMessages(), + }), + path: ['enterprise_search', 'analytics', 'collections', 'delete'], + selectors: ({ selectors }) => ({ + isLoading: [ + () => [selectors.status], + (status) => [Status.LOADING, Status.IDLE].includes(status), + ], + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/fetch_analytics_collection_logic.test.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/fetch_analytics_collection_logic.test.tsx new file mode 100644 index 00000000000000..3e56bd816b0a85 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/fetch_analytics_collection_logic.test.tsx @@ -0,0 +1,73 @@ +/* + * 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 { LogicMounter, mockFlashMessageHelpers } from '../../../__mocks__/kea_logic'; + +import { AnalyticsCollection } from '../../../../../common/types/analytics'; +import { HttpError, Status } from '../../../../../common/types/api'; + +import { FetchAnalyticsCollectionLogic } from './fetch_analytics_collection_logic'; + +describe('fetchAnalyticsCollectionLogic', () => { + const { mount } = new LogicMounter(FetchAnalyticsCollectionLogic); + + beforeEach(() => { + jest.clearAllMocks(); + jest.useRealTimers(); + mount(); + }); + + const DEFAULT_VALUES = { + analyticsCollection: null, + data: undefined, + isLoading: true, + status: Status.IDLE, + }; + + it('has expected default values', () => { + expect(FetchAnalyticsCollectionLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('listeners', () => { + it('calls clearFlashMessages on new makeRequest', () => { + FetchAnalyticsCollectionLogic.actions.makeRequest({} as AnalyticsCollection); + expect(mockFlashMessageHelpers.clearFlashMessages).toHaveBeenCalledTimes(1); + }); + + it('calls flashAPIErrors on apiError', () => { + FetchAnalyticsCollectionLogic.actions.apiError({} as HttpError); + expect(mockFlashMessageHelpers.flashAPIErrors).toHaveBeenCalledTimes(1); + expect(mockFlashMessageHelpers.flashAPIErrors).toHaveBeenCalledWith({}); + }); + + it('calls makeRequest on fetchAnalyticsCollections', async () => { + const name = 'name'; + + FetchAnalyticsCollectionLogic.actions.makeRequest = jest.fn(); + FetchAnalyticsCollectionLogic.actions.fetchAnalyticsCollection(name); + expect(FetchAnalyticsCollectionLogic.actions.makeRequest).toHaveBeenCalledWith({ + name, + }); + }); + }); + + describe('selectors', () => { + describe('analyticsCollections', () => { + it('updates when apiSuccess listener triggered', () => { + FetchAnalyticsCollectionLogic.actions.apiSuccess({} as AnalyticsCollection); + + expect(FetchAnalyticsCollectionLogic.values).toEqual({ + ...DEFAULT_VALUES, + analyticsCollection: {}, + data: {}, + isLoading: false, + status: Status.SUCCESS, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/fetch_analytics_collection_logic.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/fetch_analytics_collection_logic.ts new file mode 100644 index 00000000000000..819936ff539048 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/fetch_analytics_collection_logic.ts @@ -0,0 +1,57 @@ +/* + * 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 { kea, MakeLogicType } from 'kea'; + +import { AnalyticsCollection } from '../../../../../common/types/analytics'; +import { Status } from '../../../../../common/types/api'; +import { Actions } from '../../../shared/api_logic/create_api_logic'; +import { flashAPIErrors, clearFlashMessages } from '../../../shared/flash_messages'; +import { + FetchAnalyticsCollectionAPILogic, + FetchAnalyticsCollectionApiLogicResponse, +} from '../../api/fetch_analytics_collection/fetch_analytics_collection_api_logic'; + +export interface FetchAnalyticsCollectionActions { + apiError: Actions<{}, FetchAnalyticsCollectionApiLogicResponse>['apiError']; + apiSuccess: Actions<{}, FetchAnalyticsCollectionApiLogicResponse>['apiSuccess']; + fetchAnalyticsCollection(name: string): AnalyticsCollection; + makeRequest: Actions<{}, FetchAnalyticsCollectionApiLogicResponse>['makeRequest']; +} +export interface FetchAnalyticsCollectionValues { + analyticsCollection: AnalyticsCollection; + data: typeof FetchAnalyticsCollectionAPILogic.values.data; + isLoading: boolean; + status: Status; +} + +export const FetchAnalyticsCollectionLogic = kea< + MakeLogicType<FetchAnalyticsCollectionValues, FetchAnalyticsCollectionActions> +>({ + actions: { + fetchAnalyticsCollection: (name) => ({ name }), + }, + connect: { + actions: [FetchAnalyticsCollectionAPILogic, ['makeRequest', 'apiSuccess', 'apiError']], + values: [FetchAnalyticsCollectionAPILogic, ['data', 'status']], + }, + listeners: ({ actions }) => ({ + apiError: (e) => flashAPIErrors(e), + fetchAnalyticsCollection: ({ name }) => { + actions.makeRequest({ name }); + }, + makeRequest: () => clearFlashMessages(), + }), + path: ['enterprise_search', 'analytics', 'collection'], + selectors: ({ selectors }) => ({ + analyticsCollection: [() => [selectors.data], (data) => data || null], + isLoading: [ + () => [selectors.status], + (status) => [Status.LOADING, Status.IDLE].includes(status), + ], + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_table.test.tsx index bb5e2bfb324c20..8ca63956f6b796 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_table.test.tsx @@ -39,6 +39,6 @@ describe('AnalyticsCollectionTable', () => { expect(rows).toHaveLength(1); expect(rows[0]).toMatchObject(analyticsCollections[0]); - expect(wrapper.dive().find(EuiLinkTo).first().prop('to')).toBe('/collections/example'); + expect(wrapper.dive().find(EuiLinkTo).first().prop('to')).toBe('/collections/example/events'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_table.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_table.tsx index 6ce6e677b5a266..6a983fbd5587f9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_table.tsx @@ -39,6 +39,7 @@ export const AnalyticsCollectionTable: React.FC<AnalyticsCollectionTableProps> = <EuiLinkTo to={generateEncodedPath(COLLECTION_VIEW_PATH, { name, + section: 'events', })} > {name} @@ -57,6 +58,7 @@ export const AnalyticsCollectionTable: React.FC<AnalyticsCollectionTableProps> = navigateToUrl( generateEncodedPath(COLLECTION_VIEW_PATH, { name: collection.name, + section: 'events', }) ), type: 'icon', diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/index.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/index.tsx index 5b41c1e1a653c2..b2f7d20bf261c9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/index.tsx @@ -14,9 +14,10 @@ import { VersionMismatchPage } from '../shared/version_mismatch'; import { AddAnalyticsCollection } from './components/add_analytics_collections/add_analytics_collection'; +import { AnalyticsCollectionView } from './components/analytics_collection_view/analytics_collection_view'; import { AnalyticsOverview } from './components/analytics_overview/analytics_overview'; -import { ROOT_PATH, COLLECTION_CREATION_PATH } from './routes'; +import { ROOT_PATH, COLLECTION_CREATION_PATH, COLLECTION_VIEW_PATH } from './routes'; export const Analytics: React.FC<InitialAppData> = (props) => { const { enterpriseSearchVersion, kibanaVersion } = props; @@ -37,6 +38,9 @@ export const Analytics: React.FC<InitialAppData> = (props) => { <Route path={COLLECTION_CREATION_PATH}> <AddAnalyticsCollection /> </Route> + <Route exact path={COLLECTION_VIEW_PATH}> + <AnalyticsCollectionView /> + </Route> </Switch> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/routes.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/routes.ts index 66803c3e87b10b..14532ecfd50797 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/routes.ts @@ -8,4 +8,4 @@ export const ROOT_PATH = '/'; export const COLLECTIONS_PATH = '/collections'; export const COLLECTION_CREATION_PATH = `${COLLECTIONS_PATH}/new`; -export const COLLECTION_VIEW_PATH = `${COLLECTIONS_PATH}/:name`; +export const COLLECTION_VIEW_PATH = `${COLLECTIONS_PATH}/:name/:section`; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/cypress.config.js b/x-pack/plugins/enterprise_search/public/applications/app_search/cypress.config.js new file mode 100644 index 00000000000000..b625b3051f15a4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/cypress.config.js @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import { defineConfig } from 'cypress'; + +// eslint-disable-next-line import/no-default-export +export default defineConfig({ + defaultCommandTimeout: 120000, + e2e: { + baseUrl: 'http://localhost:5601', + // eslint-disable-next-line no-unused-vars + setupNodeEvents(on, config) {}, + supportFile: './cypress/support/commands.ts', + }, + env: { + password: 'changeme', + username: 'elastic', + }, + execTimeout: 120000, + pageLoadTimeout: 180000, + retries: { + runMode: 2, + }, + screenshotsFolder: '../../../target/cypress/screenshots', + video: false, + videosFolder: '../../../target/cypress/videos', + viewportHeight: 1200, + viewportWidth: 1600, +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/cypress.json b/x-pack/plugins/enterprise_search/public/applications/app_search/cypress.json deleted file mode 100644 index 766aaf6df36ada..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/cypress.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "supportFile": "./cypress/support/commands.ts", - "pluginsFile": false, - "retries": { - "runMode": 2 - }, - "baseUrl": "http://localhost:5601", - "env": { - "username": "elastic", - "password": "changeme" - }, - "screenshotsFolder": "../../../target/cypress/screenshots", - "videosFolder": "../../../target/cypress/videos", - "defaultCommandTimeout": 120000, - "execTimeout": 120000, - "pageLoadTimeout": 180000, - "viewportWidth": 1600, - "viewportHeight": 1200, - "video": false -} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/cypress/integration/engines.spec.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/cypress/e2e/engines.cy.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/app_search/cypress/integration/engines.spec.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/cypress/e2e/engines.cy.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/licensing_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/licensing_callout.tsx index ec514dfc5b5b12..8325b1a5305ee1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/licensing_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/licensing_callout.tsx @@ -9,59 +9,90 @@ import React from 'react'; import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; import { docLinks } from '../../../shared/doc_links/doc_links'; -export const LicensingCallout: React.FC = () => ( - <EuiCallOut - title={i18n.translate('xpack.enterpriseSearch.content.licensingCallout.title', { - defaultMessage: 'Platinum features', - })} - > - <p> - {i18n.translate('xpack.enterpriseSearch.content.licensingCallout.contentOne', { +export enum LICENSING_FEATURE { + NATIVE_CONNECTOR = 'nativeConnector', + CRAWLER = 'crawler', + INFERENCE = 'inference', +} + +type ContentBlock = Record<LICENSING_FEATURE, string>; + +export const LicensingCallout: React.FC<{ feature: LICENSING_FEATURE }> = ({ feature }) => { + const firstContentBlock: ContentBlock = { + [LICENSING_FEATURE.NATIVE_CONNECTOR]: i18n.translate( + 'xpack.enterpriseSearch.content.licensingCallout.nativeConnector.contentOne', + { defaultMessage: - 'This feature requires a Platinum license or higher. From 8.5 this feature will be unavailable to Standard license self-managed deployments.', - })} - </p> - <p> - <FormattedMessage - id="xpack.enterpriseSearch.content.licensingCallout.contentTwoDetail" - defaultMessage="You will continue to be able to use web crawlers created in previous versions in 8.5. However, you won't be able to create {strongNew} web crawlers without a Platinum license or higher." - values={{ - strongNew: ( - <strong> - <FormattedMessage - id="xpack.enterpriseSearch.content.licensingCallout.contentTwoStrongNew" - defaultMessage="new" - /> - </strong> - ), - }} - /> - </p> - <p> - {i18n.translate('xpack.enterpriseSearch.content.licensingCallout.contentThree', { + 'Built-in connectors require a Platinum license or higher and are not available to Standard license self-managed deployments. You need to upgrade to use this feature.', + } + ), + [LICENSING_FEATURE.CRAWLER]: i18n.translate( + 'xpack.enterpriseSearch.content.licensingCallout.crawler.contentOne', + { defaultMessage: - "Did you know that the web crawler is available with a Standard Elastic Cloud license? Elastic Cloud gives you the flexibility to run where you want. Deploy our managed service on Google Cloud, Microsoft Azure, or Amazon Web Services, and we'll handle the maintenance and upkeep for you.", + 'The web crawler requires a Platinum license or higher and is not available to Standard license self-managed deployments. You need to upgrade to use this feature.', + } + ), + [LICENSING_FEATURE.INFERENCE]: i18n.translate( + 'xpack.enterpriseSearch.content.licensingCallout.inference.contentOne', + { + defaultMessage: + 'Inference processors require a Platinum license or higher and are not available to Standard license self-managed deployments. You need to upgrade to use this feature.', + } + ), + }; + + const secondContentBlock: ContentBlock = { + [LICENSING_FEATURE.NATIVE_CONNECTOR]: i18n.translate( + 'xpack.enterpriseSearch.content.licensingCallout.contentTwo', + { + defaultMessage: + "Did you know that built-in connectors are available with a Standard Elastic Cloud license? Elastic Cloud gives you the flexibility to run where you want. Deploy our managed service on Google Cloud, Microsoft Azure, or Amazon Web Services, and we'll handle the maintenance and upkeep for you.", + } + ), + [LICENSING_FEATURE.CRAWLER]: i18n.translate( + 'xpack.enterpriseSearch.content.licensingCallout.crawler.contentTwo', + { + defaultMessage: + "Did you know that web crawlers are available with a Standard Elastic Cloud license? Elastic Cloud gives you the flexibility to run where you want. Deploy our managed service on Google Cloud, Microsoft Azure, or Amazon Web Services, and we'll handle the maintenance and upkeep for you.", + } + ), + [LICENSING_FEATURE.INFERENCE]: i18n.translate( + 'xpack.enterpriseSearch.content.licensingCallout.inference.contentTwo', + { + defaultMessage: + "Did you know that inference processors are available with a Standard Elastic Cloud license? Elastic Cloud gives you the flexibility to run where you want. Deploy our managed service on Google Cloud, Microsoft Azure, or Amazon Web Services, and we'll handle the maintenance and upkeep for you.", + } + ), + }; + + return ( + <EuiCallOut + title={i18n.translate('xpack.enterpriseSearch.content.licensingCallout.title', { + defaultMessage: 'Platinum features', })} - </p> - <EuiFlexGroup> - <EuiFlexItem> - <EuiLink external href={docLinks.licenseManagement}> - {i18n.translate('xpack.enterpriseSearch.workplaceSearch.explorePlatinumFeatures.link', { - defaultMessage: 'Explore Platinum features', - })} - </EuiLink> - </EuiFlexItem> - <EuiFlexItem> - <EuiLink href="https://www.elastic.co/cloud/elasticsearch-service/signup" external> - {i18n.translate('xpack.enterpriseSearch.content.licensingCallout.contentCloudTrial', { - defaultMessage: 'Sign up for a free 14-day Elastic Cloud trial.', - })} - </EuiLink> - </EuiFlexItem> - </EuiFlexGroup> - </EuiCallOut> -); + > + <p>{firstContentBlock[feature]}</p> + <p>{secondContentBlock[feature]}</p> + <EuiFlexGroup> + <EuiFlexItem> + <EuiLink external href={docLinks.licenseManagement}> + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.explorePlatinumFeatures.link', { + defaultMessage: 'Explore Platinum features', + })} + </EuiLink> + </EuiFlexItem> + <EuiFlexItem> + <EuiLink href="https://www.elastic.co/cloud/elasticsearch-service/signup" external> + {i18n.translate('xpack.enterpriseSearch.content.licensingCallout.contentCloudTrial', { + defaultMessage: 'Sign up for a free 14-day Elastic Cloud trial.', + })} + </EuiLink> + </EuiFlexItem> + </EuiFlexGroup> + </EuiCallOut> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_connector/method_connector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_connector/method_connector.tsx index fe62dd439e3a3b..70cf83fbb97645 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_connector/method_connector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_connector/method_connector.tsx @@ -9,7 +9,14 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { EuiConfirmModal, EuiLink, EuiSteps, EuiText } from '@elastic/eui'; +import { + EuiConfirmModal, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiSteps, + EuiText, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -17,8 +24,11 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { Status } from '../../../../../../common/types/api'; import { docLinks } from '../../../../shared/doc_links'; +import { KibanaLogic } from '../../../../shared/kibana'; +import { LicensingLogic } from '../../../../shared/licensing'; import { AddConnectorApiLogic } from '../../../api/connector/add_connector_api_logic'; +import { LicensingCallout, LICENSING_FEATURE } from '../licensing_callout'; import { CREATE_ELASTICSEARCH_INDEX_STEP, BUILD_SEARCH_EXPERIENCE_STEP } from '../method_steps'; import { NewSearchIndexLogic } from '../new_search_index_logic'; import { NewSearchIndexTemplate } from '../new_search_index_template'; @@ -33,134 +43,151 @@ export const MethodConnector: React.FC<{ isNative: boolean }> = ({ isNative }) = const { isModalVisible } = useValues(AddConnectorLogic); const { setIsModalVisible } = useActions(AddConnectorLogic); const { fullIndexName, language } = useValues(NewSearchIndexLogic); + const { isCloud } = useValues(KibanaLogic); + const { hasPlatinumLicense } = useValues(LicensingLogic); + + const isGated = isNative && !isCloud && !hasPlatinumLicense; return ( - <NewSearchIndexTemplate - docsUrl="https://github.com/elastic/connectors-ruby/blob/main/README.md" - error={errorToText(error)} - title={i18n.translate('xpack.enterpriseSearch.content.newIndex.steps.buildConnector.title', { - defaultMessage: 'Build a connector', - })} - type="connector" - onNameChange={() => { - apiReset(); - }} - onSubmit={(name, lang) => makeRequest({ indexName: name, isNative, language: lang })} - buttonLoading={status === Status.LOADING} - > - <EuiSteps - steps={[ - CREATE_ELASTICSEARCH_INDEX_STEP, - isNative - ? { - children: ( - <EuiText size="s"> - <p> - <FormattedMessage - id="xpack.enterpriseSearch.content.newIndex.steps.nativeConnector.content" - defaultMessage="Using our built-in connectors, you’ll be able to ingest data into your Elasticsearch index easily and swiftly using a number of Elastic-developed connectors." - /> - </p> - </EuiText> - ), - status: 'incomplete', - title: i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.methodConnector.steps.nativeConnector.title', - { - defaultMessage: 'Use a pre-built connector to populate your index', - } - ), - titleSize: 'xs', - } - : { - children: isNative ? ( - <EuiText size="s"> - <p> - <FormattedMessage - id="xpack.enterpriseSearch.content.newIndex.steps.nativeConnector.content" - defaultMessage="Using our built-in connectors, you’ll be able to ingest data into your Elasticsearch index easily and swiftly using a number of Elastic-developed connectors." - /> - </p> - </EuiText> - ) : ( - <EuiText size="s"> - <p> - <FormattedMessage - id="xpack.enterpriseSearch.content.newIndex.steps.buildConnector.content" - defaultMessage="Using our connector framework and connector client examples, you’ll be able to accelerate ingestion to the Elasticsearch {bulkApiDocLink} for any data source. After creating your index, you will be guided through the steps to access the connector framework and connect your first connector client." - values={{ - bulkApiDocLink: ( - <EuiLink href={docLinks.bulkApi} target="_blank" external> - {i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.methodConnector.steps.buildConnector.bulkAPILink', - { defaultMessage: 'Bulk API' } - )} - </EuiLink> - ), - }} - /> - </p> - </EuiText> - ), - status: 'incomplete', - title: i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.methodConnector.steps.buildConnector.title', - { - defaultMessage: 'Build and configure a connector', - } - ), - titleSize: 'xs', - }, - BUILD_SEARCH_EXPERIENCE_STEP, - ]} - /> - {isModalVisible && ( - <EuiConfirmModal + <EuiFlexGroup direction="column"> + {isGated && ( + <EuiFlexItem> + <LicensingCallout feature={LICENSING_FEATURE.NATIVE_CONNECTOR} /> + </EuiFlexItem> + )} + <EuiFlexItem> + <NewSearchIndexTemplate + docsUrl="https://github.com/elastic/connectors-ruby/blob/main/README.md" + disabled={isGated} + error={errorToText(error)} title={i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.title', + 'xpack.enterpriseSearch.content.newIndex.steps.buildConnector.title', { - defaultMessage: 'Replace existing connector', + defaultMessage: 'Build a connector', } )} - onCancel={(event) => { - event?.preventDefault(); - setIsModalVisible(false); + type="connector" + onNameChange={() => { + apiReset(); }} - onConfirm={(event) => { - event.preventDefault(); - makeRequest({ - deleteExistingConnector: true, - indexName: fullIndexName, - isNative, - language, - }); - }} - cancelButtonText={i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.cancelButton.label', - { - defaultMessage: 'Cancel', - } - )} - confirmButtonText={i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.confirmButton.label', - { - defaultMessage: 'Replace configuration', - } - )} - defaultFocusedButton="confirm" + onSubmit={(name, lang) => makeRequest({ indexName: name, isNative, language: lang })} + buttonLoading={status === Status.LOADING} > - {i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.description', - { - defaultMessage: - 'A deleted index named {indexName} was originally tied to an existing connector configuration. Would you like to replace the existing connector configuration with a new one?', - values: { - indexName: fullIndexName, - }, - } + <EuiSteps + steps={[ + CREATE_ELASTICSEARCH_INDEX_STEP, + isNative + ? { + children: ( + <EuiText size="s"> + <p> + <FormattedMessage + id="xpack.enterpriseSearch.content.newIndex.steps.nativeConnector.content" + defaultMessage="Using our built-in connectors, you’ll be able to ingest data into your Elasticsearch index easily and swiftly using a number of Elastic-developed connectors." + /> + </p> + </EuiText> + ), + status: 'incomplete', + title: i18n.translate( + 'xpack.enterpriseSearch.content.newIndex.methodConnector.steps.nativeConnector.title', + { + defaultMessage: 'Use a pre-built connector to populate your index', + } + ), + titleSize: 'xs', + } + : { + children: isNative ? ( + <EuiText size="s"> + <p> + <FormattedMessage + id="xpack.enterpriseSearch.content.newIndex.steps.nativeConnector.content" + defaultMessage="Using our built-in connectors, you’ll be able to ingest data into your Elasticsearch index easily and swiftly using a number of Elastic-developed connectors." + /> + </p> + </EuiText> + ) : ( + <EuiText size="s"> + <p> + <FormattedMessage + id="xpack.enterpriseSearch.content.newIndex.steps.buildConnector.content" + defaultMessage="Using our connector framework and connector client examples, you’ll be able to accelerate ingestion to the Elasticsearch {bulkApiDocLink} for any data source. After creating your index, you will be guided through the steps to access the connector framework and connect your first connector client." + values={{ + bulkApiDocLink: ( + <EuiLink href={docLinks.bulkApi} target="_blank" external> + {i18n.translate( + 'xpack.enterpriseSearch.content.newIndex.methodConnector.steps.buildConnector.bulkAPILink', + { defaultMessage: 'Bulk API' } + )} + </EuiLink> + ), + }} + /> + </p> + </EuiText> + ), + status: 'incomplete', + title: i18n.translate( + 'xpack.enterpriseSearch.content.newIndex.methodConnector.steps.buildConnector.title', + { + defaultMessage: 'Build and configure a connector', + } + ), + titleSize: 'xs', + }, + BUILD_SEARCH_EXPERIENCE_STEP, + ]} + /> + {isModalVisible && ( + <EuiConfirmModal + title={i18n.translate( + 'xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.title', + { + defaultMessage: 'Replace existing connector', + } + )} + onCancel={(event) => { + event?.preventDefault(); + setIsModalVisible(false); + }} + onConfirm={(event) => { + event.preventDefault(); + makeRequest({ + deleteExistingConnector: true, + indexName: fullIndexName, + isNative, + language, + }); + }} + cancelButtonText={i18n.translate( + 'xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.cancelButton.label', + { + defaultMessage: 'Cancel', + } + )} + confirmButtonText={i18n.translate( + 'xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.confirmButton.label', + { + defaultMessage: 'Replace configuration', + } + )} + defaultFocusedButton="confirm" + > + {i18n.translate( + 'xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.description', + { + defaultMessage: + 'A deleted index named {indexName} was originally tied to an existing connector configuration. Would you like to replace the existing connector configuration with a new one?', + values: { + indexName: fullIndexName, + }, + } + )} + </EuiConfirmModal> )} - </EuiConfirmModal> - )} - </NewSearchIndexTemplate> + </NewSearchIndexTemplate> + </EuiFlexItem> + </EuiFlexGroup> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_crawler/method_crawler.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_crawler/method_crawler.tsx index 296911022ee906..b96afab404e68f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_crawler/method_crawler.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_crawler/method_crawler.tsx @@ -18,7 +18,7 @@ import { docLinks } from '../../../../shared/doc_links'; import { KibanaLogic } from '../../../../shared/kibana'; import { LicensingLogic } from '../../../../shared/licensing'; import { CreateCrawlerIndexApiLogic } from '../../../api/crawler/create_crawler_index_api_logic'; -import { LicensingCallout } from '../licensing_callout'; +import { LicensingCallout, LICENSING_FEATURE } from '../licensing_callout'; import { CREATE_ELASTICSEARCH_INDEX_STEP, BUILD_SEARCH_EXPERIENCE_STEP } from '../method_steps'; import { NewSearchIndexTemplate } from '../new_search_index_template'; @@ -30,13 +30,15 @@ export const MethodCrawler: React.FC = () => { const { isCloud } = useValues(KibanaLogic); const { hasPlatinumLicense } = useValues(LicensingLogic); + const isGated = !isCloud && !hasPlatinumLicense; + MethodCrawlerLogic.mount(); return ( <EuiFlexGroup direction="column"> - {!isCloud && !hasPlatinumLicense && ( + {isGated && ( <EuiFlexItem> - <LicensingCallout /> + <LicensingCallout feature={LICENSING_FEATURE.CRAWLER} /> </EuiFlexItem> )} <EuiFlexItem> @@ -49,6 +51,7 @@ export const MethodCrawler: React.FC = () => { )} type="crawler" onSubmit={(indexName, language) => makeRequest({ indexName, language })} + disabled={isGated} buttonLoading={status === Status.LOADING} docsUrl={docLinks.crawlerOverview} > diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/new_search_index_template.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/new_search_index_template.tsx index 69baaee26488fa..6401db2a7974df 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/new_search_index_template.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/new_search_index_template.tsx @@ -32,6 +32,7 @@ import { LanguageForOptimization } from './types'; export interface Props { buttonLoading?: boolean; + disabled?: boolean; docsUrl?: string; error?: string | React.ReactNode; onNameChange?(name: string): void; @@ -42,6 +43,7 @@ export interface Props { export const NewSearchIndexTemplate: React.FC<Props> = ({ children, + disabled, docsUrl, error, title, @@ -101,12 +103,12 @@ export const NewSearchIndexTemplate: React.FC<Props> = ({ return ( <EuiPanel hasBorder> <EuiForm + component="form" + id="enterprise-search-add-connector" onSubmit={(event) => { event.preventDefault(); onSubmit(fullIndexName, language); }} - component="form" - id="enterprise-search-add-connector" > <EuiFlexGroup direction="column"> <EuiFlexItem grow={false}> @@ -118,6 +120,7 @@ export const NewSearchIndexTemplate: React.FC<Props> = ({ <EuiFlexGroup> <EuiFlexItem grow> <EuiFormRow + isDisabled={disabled} label={i18n.translate( 'xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.nameInputLabel', { @@ -145,6 +148,7 @@ export const NewSearchIndexTemplate: React.FC<Props> = ({ } )} fullWidth + disabled={disabled} isInvalid={false} value={rawName} onChange={handleNameChange} @@ -164,6 +168,7 @@ export const NewSearchIndexTemplate: React.FC<Props> = ({ </EuiFlexItem> <EuiFlexItem grow={false}> <EuiFormRow + isDisabled={disabled} label={i18n.translate( 'xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.languageInputLabel', { @@ -178,6 +183,7 @@ export const NewSearchIndexTemplate: React.FC<Props> = ({ )} > <EuiSelect + disabled={disabled} options={SUPPORTED_LANGUAGES} onChange={handleLanguageChange} value={languageSelectValue} @@ -192,7 +198,7 @@ export const NewSearchIndexTemplate: React.FC<Props> = ({ <EuiFlexItem grow={false}> <EuiButton fill - isDisabled={!rawName || buttonLoading || formInvalid} + isDisabled={!rawName || buttonLoading || formInvalid || disabled} isLoading={buttonLoading} type="submit" > diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling.tsx index 9c22ecd8572ef8..ca9a415c4c958d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling.tsx @@ -34,7 +34,7 @@ import { UpdateConnectorSchedulingApiLogic } from '../../../api/connector/update import { SEARCH_INDEX_TAB_PATH } from '../../../routes'; import { IngestionStatus } from '../../../types'; -import { isConnectorIndex, isConnectorCrawlerIndex } from '../../../utils/indices'; +import { isConnectorIndex } from '../../../utils/indices'; import { IndexViewLogic } from '../index_view_logic'; @@ -61,7 +61,7 @@ export const ConnectorSchedulingComponent: React.FC = () => { frequency: schedulingInput?.interval ? cronToFrequency(schedulingInput.interval) : 'HOUR', }); - if (!isConnectorIndex(index) && !isConnectorCrawlerIndex(index)) { + if (!isConnectorIndex(index)) { return <></>; } diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.test.tsx new file mode 100644 index 00000000000000..fab7620f7f2327 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.test.tsx @@ -0,0 +1,47 @@ +/* + * 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 { setMockValues } from '../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiBadge, EuiHealth, EuiPanel, EuiTitle } from '@elastic/eui'; + +import { InferencePipelineCard } from './inference_pipeline_card'; + +export const DEFAULT_VALUES = { + pipelineName: 'Sample Processor', + trainedModelName: 'example_trained_model', + isDeployed: true, + modelType: 'pytorch', +}; + +const mockValues = { ...DEFAULT_VALUES }; + +describe('InfererencePipelineCard', () => { + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(mockValues); + }); + it('renders the item', () => { + const wrapper = shallow(<InferencePipelineCard {...mockValues} />); + expect(wrapper.find(EuiPanel)).toHaveLength(1); + expect(wrapper.find(EuiTitle)).toHaveLength(1); + expect(wrapper.find(EuiBadge)).toHaveLength(1); + + const health = wrapper.find(EuiHealth); + expect(health.prop('children')).toEqual('Deployed'); + }); + + it('renders an undeployed item', () => { + const wrapper = shallow(<InferencePipelineCard {...mockValues} isDeployed={false} />); + const health = wrapper.find(EuiHealth); + expect(health.prop('children')).toEqual('Not deployed'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.tsx new file mode 100644 index 00000000000000..32547f81f00c08 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.tsx @@ -0,0 +1,124 @@ +/* + * 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, { useState } from 'react'; + +import { + EuiBadge, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiPanel, + EuiPopover, + EuiTextColor, + EuiTitle, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { InferencePipeline } from './types'; + +export const InferencePipelineCard: React.FC<InferencePipeline> = ({ + pipelineName, + trainedModelName, + isDeployed, + modelType, +}) => { + const [isPopOverOpen, setIsPopOverOpen] = useState(false); + + const deployedText = i18n.translate('xpack.enterpriseSearch.inferencePipelineCard.isDeployed', { + defaultMessage: 'Deployed', + }); + + const notDeployedText = i18n.translate( + 'xpack.enterpriseSearch.inferencePipelineCard.isNotDeployed', + { defaultMessage: 'Not deployed' } + ); + + const actionButton = ( + <EuiButtonEmpty + iconSide="right" + flush="both" + iconType="boxesVertical" + onClick={() => setIsPopOverOpen(!isPopOverOpen)} + > + {i18n.translate('xpack.enterpriseSearch.inferencePipelineCard.actionButton', { + defaultMessage: 'Actions', + })} + </EuiButtonEmpty> + ); + + return ( + <EuiPanel color="subdued"> + <EuiFlexGroup direction="column" gutterSize="xs"> + <EuiFlexItem> + <EuiFlexGroup alignItems="center"> + <EuiFlexItem> + <EuiTitle size="xs"> + <h4>{pipelineName}</h4> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiPopover + button={actionButton} + isOpen={isPopOverOpen} + closePopover={() => setIsPopOverOpen(false)} + > + <EuiFlexGroup direction="column" gutterSize="none"> + <EuiFlexItem> + <div> + <EuiButtonEmpty flush="both" iconType="eye" color="text"> + {i18n.translate( + 'xpack.enterpriseSearch.inferencePipelineCard.action.view', + { defaultMessage: 'View pipeline in Stack Management' } + )} + </EuiButtonEmpty> + </div> + </EuiFlexItem> + <EuiFlexItem> + <div> + <EuiButtonEmpty flush="both" iconType="trash" color="text"> + {i18n.translate( + 'xpack.enterpriseSearch.inferencePipelineCard.action.delete', + { defaultMessage: 'Delete pipeline' } + )} + </EuiButtonEmpty> + </div> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPopover> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem> + <EuiFlexGroup> + <EuiFlexItem> + <EuiTextColor color="subdued">{trainedModelName}</EuiTextColor> + </EuiFlexItem> + <EuiFlexItem> + <EuiFlexGroup gutterSize="m" justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <EuiHealth color={isDeployed ? 'success' : 'accent'}> + {isDeployed ? deployedText : notDeployedText} + </EuiHealth> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFlexGroup gutterSize="xs"> + <EuiFlexItem> + <EuiBadge color="hollow">{modelType}</EuiBadge> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx new file mode 100644 index 00000000000000..84ca76685926d5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx @@ -0,0 +1,142 @@ +/* + * 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 from 'react'; + +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { DataPanel } from '../../../../shared/data_panel/data_panel'; + +import { InferencePipelineCard } from './inference_pipeline_card'; +import { InferencePipeline } from './types'; + +export const SearchIndexPipelines: React.FC = () => { + // TODO: REPLACE THIS DATA WITH REAL DATA + + const inferencePipelines: InferencePipeline[] = [ + { + pipelineName: 'NER Processor', + trainedModelName: 'elastic_dslim_bert_base_ner', + isDeployed: true, + modelType: 'pytorch', + }, + { + pipelineName: 'Sentiment Analysis', + trainedModelName: 'elastic_dslim_bert_base_ner', + isDeployed: false, + modelType: 'pytorch', + }, + { + pipelineName: 'Sentiment Analysis', + trainedModelName: 'elastic_dslim_bert_base_ner', + isDeployed: false, + modelType: 'pytorch', + }, + ]; + + return ( + <> + <EuiSpacer /> + <EuiFlexGroup direction="row"> + <EuiFlexItem> + <DataPanel + hasBorder + footerDocLink={ + <EuiLink href="" external color="subdued"> + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.ingestionPipeline.docLink', + { + defaultMessage: 'Learn more about using pipelines in Enterprise Search', + } + )} + </EuiLink> + } + title={ + <h2> + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.ingestionPipeline.title', + { + defaultMessage: 'Ingest Pipelines', + } + )} + </h2> + } + subtitle={i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.ingestionPipeline.subtitle', + { + defaultMessage: 'Ingest pipelines optimize your index for search applications', + } + )} + iconType="logstashInput" + > + <div /> + </DataPanel> + </EuiFlexItem> + <EuiFlexItem> + <DataPanel + hasBorder + footerDocLink={ + <EuiLink href="" external color="subdued"> + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.mlInferencePipelines.docLink', + { + defaultMessage: 'Learn more about deploying ML models in Elastic', + } + )} + </EuiLink> + } + title={ + <h2> + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.mlInferencePipelines.title', + { + defaultMessage: 'ML Inference pipelines', + } + )} + </h2> + } + subtitle={i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.mlInferencePipelines.subtitle', + { + defaultMessage: + 'Inference pipelines will be run as processors from the Enterprise Search Ingest Pipeline', + } + )} + iconType="compute" + action={ + <EuiButton color="success" size="s" iconType="plusInCircle"> + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.mlInferencePipelines.newButton', + { + defaultMessage: 'Add ML inference pipeline', + } + )} + </EuiButton> + } + > + {inferencePipelines.length > 0 && ( + <EuiFlexGroup direction="column" gutterSize="s"> + {inferencePipelines.map((item: InferencePipeline, index: number) => ( + <EuiFlexItem key={index}> + <InferencePipelineCard + trainedModelName={item.trainedModelName} + pipelineName={item.pipelineName} + isDeployed={item.isDeployed} + modelType={item.modelType} + /> + </EuiFlexItem> + ))} + </EuiFlexGroup> + )} + </DataPanel> + </EuiFlexItem> + </EuiFlexGroup> + </> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/types.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/types.ts new file mode 100644 index 00000000000000..9438ce5ef4bc1e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/types.ts @@ -0,0 +1,13 @@ +/* + * 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 interface InferencePipeline { + pipelineName: string; + trainedModelName: string; + isDeployed: boolean; + modelType: string; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/search_index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/search_index.tsx index a376b4dd5bd48b..58494595ad2e40 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/search_index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/search_index.tsx @@ -17,7 +17,7 @@ import { i18n } from '@kbn/i18n'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { Status } from '../../../../../common/types/api'; -import { enableIndexTransformsTab } from '../../../../../common/ui_settings_keys'; +import { enableIndexPipelinesTab } from '../../../../../common/ui_settings_keys'; import { generateEncodedPath } from '../../../shared/encode_path_params'; import { KibanaLogic } from '../../../shared/kibana'; import { FetchIndexApiLogic } from '../../api/index/fetch_index_api_logic'; @@ -32,22 +32,24 @@ import { IndexCreatedCallout } from './components/index_created_callout/callout' import { IndexCreatedCalloutLogic } from './components/index_created_callout/callout_logic'; import { ConnectorConfiguration } from './connector/connector_configuration'; import { ConnectorSchedulingComponent } from './connector/connector_scheduling'; +import { AutomaticCrawlScheduler } from './crawler/automatic_crawl_scheduler/automatic_crawl_scheduler'; import { CrawlCustomSettingsFlyout } from './crawler/crawl_custom_settings_flyout/crawl_custom_settings_flyout'; import { SearchIndexDomainManagement } from './crawler/domain_management/domain_management'; import { SearchIndexDocuments } from './documents'; import { SearchIndexIndexMappings } from './index_mappings'; import { IndexNameLogic } from './index_name_logic'; import { SearchIndexOverview } from './overview'; +import { SearchIndexPipelines } from './pipelines/pipelines'; export enum SearchIndexTabId { // all indices OVERVIEW = 'overview', DOCUMENTS = 'documents', INDEX_MAPPINGS = 'index_mappings', + PIPELINES = 'pipelines', // connector indices CONFIGURATION = 'configuration', SCHEDULING = 'scheduling', - TRANSFORMS = 'transforms', // crawler indices DOMAIN_MANAGEMENT = 'domain_management', } @@ -64,7 +66,7 @@ export const SearchIndex: React.FC = () => { const { indexName } = useValues(IndexNameLogic); - const transformsEnabled = uiSettings?.get<boolean>(enableIndexTransformsTab) ?? false; + const pipelinesEnabled = uiSettings?.get<boolean>(enableIndexPipelinesTab) ?? false; const ALL_INDICES_TABS: EuiTabbedContentTab[] = [ { @@ -116,7 +118,7 @@ export const SearchIndex: React.FC = () => { }), }, { - content: <ConnectorSchedulingComponent />, + content: <AutomaticCrawlScheduler />, id: SearchIndexTabId.SCHEDULING, name: i18n.translate('xpack.enterpriseSearch.content.searchIndex.schedulingTabLabel', { defaultMessage: 'Scheduling', @@ -124,12 +126,12 @@ export const SearchIndex: React.FC = () => { }, ]; - const TRANSFORMS_TAB: EuiTabbedContentTab[] = [ + const PIPELINES_TAB: EuiTabbedContentTab[] = [ { - content: <div />, - id: SearchIndexTabId.TRANSFORMS, - name: i18n.translate('xpack.enterpriseSearch.content.searchIndex.transformsTabLabel', { - defaultMessage: 'Transforms', + content: <SearchIndexPipelines />, + id: SearchIndexTabId.PIPELINES, + name: i18n.translate('xpack.enterpriseSearch.content.searchIndex.pipelinesTabLabel', { + defaultMessage: 'Pipelines', }), }, ]; @@ -138,7 +140,7 @@ export const SearchIndex: React.FC = () => { ...ALL_INDICES_TABS, ...(isConnectorIndex(indexData) ? CONNECTOR_TABS : []), ...(isCrawlerIndex(indexData) ? CRAWLER_TABS : []), - ...(transformsEnabled && isConnectorIndex(indexData) ? TRANSFORMS_TAB : []), + ...(pipelinesEnabled ? PIPELINES_TAB : []), ]; const selectedTab = tabs.find((tab) => tab.id === tabId); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/utils/indices.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/utils/indices.test.ts index b023aff4b9e085..ce6e443a250259 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/utils/indices.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/utils/indices.test.ts @@ -23,7 +23,6 @@ import { getLastUpdated, indexToViewIndex, isConnectorIndex, - isConnectorCrawlerIndex, isCrawlerIndex, isApiIndex, isConnectorViewIndex, @@ -145,20 +144,6 @@ describe('Indices util functions', () => { expect(isConnectorIndex(apiIndex)).toEqual(false); }); }); - describe('isConnectorCrawlerIndex', () => { - it('should return false for connector indices', () => { - expect(isConnectorCrawlerIndex(connectorIndex)).toEqual(false); - }); - it('should return false for connector-crawler indices', () => { - expect(isConnectorCrawlerIndex(connectorCrawlerIndex)).toEqual(true); - }); - it('should return false for crawler indices', () => { - expect(isConnectorCrawlerIndex(crawlerIndex)).toEqual(false); - }); - it('should return false for API indices', () => { - expect(isConnectorCrawlerIndex(apiIndex)).toEqual(false); - }); - }); describe('isCrawlerIndex', () => { it('should return true for crawler indices', () => { expect(isCrawlerIndex(crawlerIndex)).toEqual(true); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/utils/indices.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/utils/indices.ts index 540ad2b2db69e2..9a17f7fe84f4dd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/utils/indices.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/utils/indices.ts @@ -13,7 +13,6 @@ import { ENTERPRISE_SEARCH_CONNECTOR_CRAWLER_SERVICE_TYPE } from '../../../../co import { SyncStatus, ConnectorStatus } from '../../../../common/types/connectors'; import { ConnectorIndex, - ConnectorCrawlerIndex, CrawlerIndex, ElasticsearchIndexWithIngestion, } from '../../../../common/types/indices'; @@ -37,16 +36,6 @@ export function isConnectorIndex( ); } -export function isConnectorCrawlerIndex( - index: ElasticsearchIndexWithIngestion | undefined -): index is ConnectorCrawlerIndex { - const crawlerIndex = index as CrawlerIndex; - return ( - !!crawlerIndex?.connector && - crawlerIndex.connector.service_type === ENTERPRISE_SEARCH_CONNECTOR_CRAWLER_SERVICE_TYPE - ); -} - export function isCrawlerIndex( index: ElasticsearchIndexWithIngestion | undefined ): index is CrawlerIndex { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/cypress.config.js b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/cypress.config.js new file mode 100644 index 00000000000000..a6d98df28c413c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/cypress.config.js @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import { defineConfig } from 'cypress'; + +// eslint-disable-next-line import/no-default-export +export default defineConfig({ + defaultCommandTimeout: 120000, + e2e: { + baseUrl: 'http://localhost:5601', + // eslint-disable-next-line no-unused-vars + setupNodeEvents(on, config) {}, + supportFile: false, + }, + env: { + password: 'changeme', + username: 'elastic', + }, + execTimeout: 120000, + fixturesFolder: false, + pageLoadTimeout: 180000, + retries: { + runMode: 2, + }, + screenshotsFolder: '../../../target/cypress/screenshots', + video: false, + videosFolder: '../../../target/cypress/videos', + viewportHeight: 1200, + viewportWidth: 1600, +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/cypress.json b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/cypress.json deleted file mode 100644 index 8ca8bdfd79a49b..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/cypress.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "supportFile": false, - "pluginsFile": false, - "retries": { - "runMode": 2 - }, - "baseUrl": "http://localhost:5601", - "env": { - "username": "elastic", - "password": "changeme" - }, - "fixturesFolder": false, - "screenshotsFolder": "../../../target/cypress/screenshots", - "videosFolder": "../../../target/cypress/videos", - "defaultCommandTimeout": 120000, - "execTimeout": 120000, - "pageLoadTimeout": 180000, - "viewportWidth": 1600, - "viewportHeight": 1200, - "video": false -} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/cypress/integration/overview.spec.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/cypress/e2e/overview.cy.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/cypress/integration/overview.spec.ts rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/cypress/e2e/overview.cy.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/data_panel/data_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/data_panel/data_panel.test.tsx index 703e4d1328e7d5..547dc87e274864 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/data_panel/data_panel.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/data_panel/data_panel.test.tsx @@ -129,7 +129,7 @@ describe('DataPanel', () => { }); it('passes hasBorder', () => { - const wrapper = shallow(<DataPanel title={<h1>Test</h1>} />); + const wrapper = shallow(<DataPanel filled title={<h1>Test</h1>} />); expect(wrapper.prop('hasBorder')).toBeFalsy(); wrapper.setProps({ hasBorder: true }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/data_panel/data_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/data_panel/data_panel.tsx index 50a30a658e8bef..0d4820a531c351 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/data_panel/data_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/data_panel/data_panel.tsx @@ -14,7 +14,7 @@ import { EuiFlexItem, EuiIcon, EuiIconProps, - EuiPanel, + EuiSplitPanel, EuiSpacer, EuiText, EuiTitle, @@ -31,6 +31,7 @@ type Props = Omit<_EuiPanelDivlike, 'title'> & { subtitle?: React.ReactNode; iconType?: EuiIconProps['type']; action?: React.ReactNode; + footerDocLink?: React.ReactNode; responsive?: boolean; filled?: boolean; isLoading?: boolean; @@ -46,6 +47,7 @@ export const DataPanel: React.FC<Props> = ({ responsive = false, filled, isLoading, + footerDocLink, className, children, ...props // e.g., data-test-subj @@ -55,43 +57,55 @@ export const DataPanel: React.FC<Props> = ({ }); return ( - <EuiPanel + <EuiSplitPanel.Outer color={filled ? 'subdued' : 'plain'} className={classes} hasShadow={false} + hasBorder={!filled} aria-busy={isLoading} {...props} > - <EuiFlexGroup justifyContent="spaceBetween" alignItems="flexStart" responsive={responsive}> - <EuiFlexItem> - <EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}> - {iconType && ( - <EuiFlexItem grow={false}> - <EuiIcon type={iconType} /> + <EuiSplitPanel.Inner> + <EuiFlexGroup direction="column" gutterSize="s" responsive={responsive}> + <EuiFlexItem> + <EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}> + <EuiFlexItem grow> + <EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}> + {iconType && ( + <EuiFlexItem grow={false}> + <EuiIcon type={iconType} /> + </EuiFlexItem> + )} + <EuiFlexItem> + <EuiTitle size={titleSize}>{title}</EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> </EuiFlexItem> + {action && <EuiFlexItem grow={false}>{action}</EuiFlexItem>} + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem> + {subtitle && ( + <> + <EuiSpacer size="s" /> + <EuiText size="s" color="subdued"> + <p>{subtitle}</p> + </EuiText> + </> )} - <EuiFlexItem> - <EuiTitle size={titleSize}>{title}</EuiTitle> - </EuiFlexItem> - </EuiFlexGroup> - {subtitle && ( - <> - <EuiSpacer size="s" /> - <EuiText size="s" color="subdued"> - <p>{subtitle}</p> - </EuiText> - </> - )} - </EuiFlexItem> - {action && <EuiFlexItem grow={false}>{action}</EuiFlexItem>} - </EuiFlexGroup> - {children && ( - <> - <EuiSpacer size={filled || subtitle ? 'l' : 's'} /> - {children} - </> + </EuiFlexItem> + </EuiFlexGroup> + {children && ( + <> + <EuiSpacer size={filled || subtitle ? 'l' : 's'} /> + {children} + </> + )} + {isLoading && <LoadingOverlay />} + </EuiSplitPanel.Inner> + {!!footerDocLink && ( + <EuiSplitPanel.Inner color="subdued">{footerDocLink}</EuiSplitPanel.Inner> )} - {isLoading && <LoadingOverlay />} - </EuiPanel> + </EuiSplitPanel.Outer> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/result/result_field.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/result/result_field.tsx index 607068d8595b56..d1922ab62de071 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/result/result_field.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/result/result_field.tsx @@ -22,7 +22,7 @@ import { ResultFieldProps } from './types'; import './result.scss'; const iconMap: Record<string, string> = { - boolean: 'tokenBoolen', + boolean: 'tokenBoolean', date: 'tokenDate', date_range: 'tokenDate', double: 'tokenNumber', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress.config.js b/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress.config.js new file mode 100644 index 00000000000000..b625b3051f15a4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress.config.js @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import { defineConfig } from 'cypress'; + +// eslint-disable-next-line import/no-default-export +export default defineConfig({ + defaultCommandTimeout: 120000, + e2e: { + baseUrl: 'http://localhost:5601', + // eslint-disable-next-line no-unused-vars + setupNodeEvents(on, config) {}, + supportFile: './cypress/support/commands.ts', + }, + env: { + password: 'changeme', + username: 'elastic', + }, + execTimeout: 120000, + pageLoadTimeout: 180000, + retries: { + runMode: 2, + }, + screenshotsFolder: '../../../target/cypress/screenshots', + video: false, + videosFolder: '../../../target/cypress/videos', + viewportHeight: 1200, + viewportWidth: 1600, +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress.json b/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress.json deleted file mode 100644 index 766aaf6df36ada..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "supportFile": "./cypress/support/commands.ts", - "pluginsFile": false, - "retries": { - "runMode": 2 - }, - "baseUrl": "http://localhost:5601", - "env": { - "username": "elastic", - "password": "changeme" - }, - "screenshotsFolder": "../../../target/cypress/screenshots", - "videosFolder": "../../../target/cypress/videos", - "defaultCommandTimeout": 120000, - "execTimeout": 120000, - "pageLoadTimeout": 180000, - "viewportWidth": 1600, - "viewportHeight": 1200, - "video": false -} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress/integration/overview.spec.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress/e2e/overview.cy.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress/integration/overview.spec.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress/e2e/overview.cy.ts diff --git a/x-pack/plugins/enterprise_search/server/index_management/setup_indices.test.ts b/x-pack/plugins/enterprise_search/server/index_management/setup_indices.test.ts index 63b777d0dff319..59e7edf1d21d5a 100644 --- a/x-pack/plugins/enterprise_search/server/index_management/setup_indices.test.ts +++ b/x-pack/plugins/enterprise_search/server/index_management/setup_indices.test.ts @@ -7,7 +7,7 @@ import { CONNECTORS_INDEX, CONNECTORS_JOBS_INDEX, CONNECTORS_VERSION } from '..'; -import { setupConnectorsIndices } from './setup_indices'; +import { defaultConnectorsPipelineMeta, setupConnectorsIndices } from './setup_indices'; describe('Setup Indices', () => { const mockClient = { @@ -29,6 +29,7 @@ describe('Setup Indices', () => { const connectorsMappings = { _meta: { version: CONNECTORS_VERSION, + pipeline: defaultConnectorsPipelineMeta, }, properties: { api_key_id: { diff --git a/x-pack/plugins/enterprise_search/server/index_management/setup_indices.ts b/x-pack/plugins/enterprise_search/server/index_management/setup_indices.ts index 08bcdbc38c3c5b..b564d519e73f99 100644 --- a/x-pack/plugins/enterprise_search/server/index_management/setup_indices.ts +++ b/x-pack/plugins/enterprise_search/server/index_management/setup_indices.ts @@ -59,11 +59,26 @@ const defaultSettings: IndicesIndexSettings = { number_of_replicas: 0, }; +export interface DefaultConnectorsPipelineMeta { + default_extract_binary_content: boolean; + default_name: string; + default_reduce_whitespace: boolean; + default_run_ml_inference: boolean; +} + +export const defaultConnectorsPipelineMeta: DefaultConnectorsPipelineMeta = { + default_extract_binary_content: true, + default_name: 'ent-search-generic-ingestion', + default_reduce_whitespace: true, + default_run_ml_inference: false, +}; + const indices: IndexDefinition[] = [ { aliases: ['.elastic-connectors'], mappings: { _meta: { + pipeline: defaultConnectorsPipelineMeta, version: '1', }, properties: connectorMappingsProperties, diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/delete_analytics_collection.test.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/delete_analytics_collection.test.ts index dbb9585dbe27c8..a321a0d118cf43 100644 --- a/x-pack/plugins/enterprise_search/server/lib/analytics/delete_analytics_collection.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/analytics/delete_analytics_collection.test.ts @@ -40,18 +40,7 @@ describe('delete analytics collection lib function', () => { }); describe('deleting analytics collections', () => { - it('should delete an analytics collection and its events indices', async () => { - const indices = [ - { - name: 'elastic_analytics-events-my-collection-12.12.12', - }, - { - name: 'elastic_analytics-events-my-collection-13.12.12', - }, - ]; - (fetchIndices as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve(indices); - }); + it('should delete an analytics collection', async () => { (fetchAnalyticsCollectionByName as jest.Mock).mockImplementationOnce(() => { return Promise.resolve({ event_retention_day_length: 180, @@ -68,11 +57,6 @@ describe('delete analytics collection lib function', () => { id: 'example-id', index: ANALYTICS_COLLECTIONS_INDEX, }); - - expect(mockClient.asCurrentUser.indices.delete).toHaveBeenCalledWith({ - ignore_unavailable: true, - index: indices.map((index) => index.name), - }); }); it('should throw an exception when analytics collection does not exist', async () => { diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/delete_analytics_collection.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/delete_analytics_collection.ts index 687296a27c13fd..d07a271bfc01c7 100644 --- a/x-pack/plugins/enterprise_search/server/lib/analytics/delete_analytics_collection.ts +++ b/x-pack/plugins/enterprise_search/server/lib/analytics/delete_analytics_collection.ts @@ -10,20 +10,9 @@ import { IScopedClusterClient } from '@kbn/core-elasticsearch-server'; import { ANALYTICS_COLLECTIONS_INDEX } from '../..'; import { ErrorCode } from '../../../common/types/error_codes'; -import { fetchIndices } from '../indices/fetch_indices'; import { fetchAnalyticsCollectionByName } from './fetch_analytics_collection'; -const deleteAnalyticsCollectionEvents = async (client: IScopedClusterClient, name: string) => { - const indexPattern = `elastic_analytics-events-${name}-*`; - const indices = await fetchIndices(client, indexPattern, true, false); - - await client.asCurrentUser.indices.delete({ - ignore_unavailable: true, - index: indices.map((index) => index.name), - }); -}; - export const deleteAnalyticsCollectionByName = async ( client: IScopedClusterClient, name: string @@ -34,8 +23,6 @@ export const deleteAnalyticsCollectionByName = async ( throw new Error(ErrorCode.ANALYTICS_COLLECTION_NOT_FOUND); } - await deleteAnalyticsCollectionEvents(client, name); - await client.asCurrentUser.delete({ id: analyticsCollection.id, index: ANALYTICS_COLLECTIONS_INDEX, diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts index 42d0235cbd3a31..24b01c5e0bf035 100644 --- a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts @@ -34,6 +34,7 @@ describe('addConnector lib function', () => { indices: { create: jest.fn(), exists: jest.fn(), + getMapping: jest.fn(), refresh: jest.fn(), }, }, @@ -49,6 +50,22 @@ describe('addConnector lib function', () => { jest.clearAllMocks(); }); + const connectorsIndicesMapping = { + '.elastic-connectors-v1': { + mappings: { + _meta: { + pipeline: { + default_extract_binary_content: true, + default_name: 'ent-search-generic-ingestion', + default_reduce_whitespace: true, + default_run_ml_inference: false, + }, + version: '1', + }, + }, + }, + }; + it('should add connector', async () => { mockClient.asCurrentUser.index.mockImplementation(() => ({ _id: 'fakeId' })); mockClient.asCurrentUser.indices.exists.mockImplementation( @@ -56,6 +73,7 @@ describe('addConnector lib function', () => { ); (fetchConnectorByIndexName as jest.Mock).mockImplementation(() => undefined); (fetchCrawlerByIndexName as jest.Mock).mockImplementation(() => undefined); + mockClient.asCurrentUser.indices.getMapping.mockImplementation(() => connectorsIndicesMapping); await expect( addConnector(mockClient as unknown as IScopedClusterClient, { @@ -76,6 +94,12 @@ describe('addConnector lib function', () => { last_sync_status: null, last_synced: null, name: 'index_name', + pipeline: { + extract_binary_content: true, + name: 'ent-search-generic-ingestion', + reduce_whitespace: true, + run_ml_inference: false, + }, scheduling: { enabled: false, interval: '0 0 0 * * ?' }, service_type: null, status: ConnectorStatus.CREATED, @@ -97,6 +121,7 @@ describe('addConnector lib function', () => { ); (fetchConnectorByIndexName as jest.Mock).mockImplementation(() => undefined); (fetchCrawlerByIndexName as jest.Mock).mockImplementation(() => undefined); + mockClient.asCurrentUser.indices.getMapping.mockImplementation(() => connectorsIndicesMapping); await expect( addConnector(mockClient as unknown as IScopedClusterClient, { @@ -115,6 +140,7 @@ describe('addConnector lib function', () => { ); (fetchConnectorByIndexName as jest.Mock).mockImplementation(() => true); (fetchCrawlerByIndexName as jest.Mock).mockImplementation(() => undefined); + mockClient.asCurrentUser.indices.getMapping.mockImplementation(() => connectorsIndicesMapping); await expect( addConnector(mockClient as unknown as IScopedClusterClient, { @@ -133,6 +159,7 @@ describe('addConnector lib function', () => { ); (fetchConnectorByIndexName as jest.Mock).mockImplementation(() => undefined); (fetchCrawlerByIndexName as jest.Mock).mockImplementation(() => true); + mockClient.asCurrentUser.indices.getMapping.mockImplementation(() => connectorsIndicesMapping); await expect( addConnector(mockClient as unknown as IScopedClusterClient, { @@ -151,6 +178,7 @@ describe('addConnector lib function', () => { ); (fetchConnectorByIndexName as jest.Mock).mockImplementation(() => true); (fetchCrawlerByIndexName as jest.Mock).mockImplementation(() => undefined); + mockClient.asCurrentUser.indices.getMapping.mockImplementation(() => connectorsIndicesMapping); await expect( addConnector(mockClient as unknown as IScopedClusterClient, { @@ -169,6 +197,7 @@ describe('addConnector lib function', () => { ); (fetchConnectorByIndexName as jest.Mock).mockImplementation(() => ({ id: 'connectorId' })); (fetchCrawlerByIndexName as jest.Mock).mockImplementation(() => undefined); + mockClient.asCurrentUser.indices.getMapping.mockImplementation(() => connectorsIndicesMapping); await expect( addConnector(mockClient as unknown as IScopedClusterClient, { @@ -194,6 +223,12 @@ describe('addConnector lib function', () => { last_sync_status: null, last_synced: null, name: 'index_name', + pipeline: { + extract_binary_content: true, + name: 'ent-search-generic-ingestion', + reduce_whitespace: true, + run_ml_inference: false, + }, scheduling: { enabled: false, interval: '0 0 0 * * ?' }, service_type: null, status: ConnectorStatus.CREATED, @@ -218,6 +253,7 @@ describe('addConnector lib function', () => { ); (fetchConnectorByIndexName as jest.Mock).mockImplementation(() => false); (fetchCrawlerByIndexName as jest.Mock).mockImplementation(() => undefined); + mockClient.asCurrentUser.indices.getMapping.mockImplementation(() => connectorsIndicesMapping); await expect( addConnector(mockClient as unknown as IScopedClusterClient, { index_name: 'search-index_name', @@ -238,6 +274,12 @@ describe('addConnector lib function', () => { last_sync_status: null, last_synced: null, name: 'index_name', + pipeline: { + extract_binary_content: true, + name: 'ent-search-generic-ingestion', + reduce_whitespace: true, + run_ml_inference: false, + }, scheduling: { enabled: false, interval: '0 0 0 * * ?' }, service_type: null, status: ConnectorStatus.CREATED, diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts index f3275c0b2d73b6..6838ef95ce936c 100644 --- a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts +++ b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts @@ -8,9 +8,13 @@ import { IScopedClusterClient } from '@kbn/core/server'; import { CONNECTORS_INDEX } from '../..'; +import { CONNECTORS_VERSION } from '../..'; import { ConnectorDocument, ConnectorStatus } from '../../../common/types/connectors'; import { ErrorCode } from '../../../common/types/error_codes'; -import { setupConnectorsIndices } from '../../index_management/setup_indices'; +import { + DefaultConnectorsPipelineMeta, + setupConnectorsIndices, +} from '../../index_management/setup_indices'; import { fetchCrawlerByIndexName } from '../crawler/fetch_crawlers'; import { createIndex } from '../indices/create_index'; @@ -67,6 +71,19 @@ export const addConnector = async ( service_type?: string | null; } ): Promise<{ id: string; index_name: string }> => { + const connectorsIndexExists = await client.asCurrentUser.indices.exists({ + index: CONNECTORS_INDEX, + }); + if (!connectorsIndexExists) { + await setupConnectorsIndices(client.asCurrentUser); + } + const connectorsIndicesMapping = await client.asCurrentUser.indices.getMapping({ + index: CONNECTORS_INDEX, + }); + const connectorsPipelineMeta: DefaultConnectorsPipelineMeta = + connectorsIndicesMapping[`${CONNECTORS_INDEX}-v${CONNECTORS_VERSION}`]?.mappings?._meta + ?.pipeline; + const document: ConnectorDocument = { api_key_id: null, configuration: {}, @@ -78,16 +95,18 @@ export const addConnector = async ( last_sync_status: null, last_synced: null, name: input.index_name.startsWith('search-') ? input.index_name.substring(7) : input.index_name, + pipeline: connectorsPipelineMeta + ? { + extract_binary_content: connectorsPipelineMeta.default_extract_binary_content, + name: connectorsPipelineMeta.default_name, + reduce_whitespace: connectorsPipelineMeta.default_reduce_whitespace, + run_ml_inference: connectorsPipelineMeta.default_run_ml_inference, + } + : null, scheduling: { enabled: false, interval: '0 0 0 * * ?' }, service_type: input.service_type || null, status: ConnectorStatus.CREATED, sync_now: false, }; - const connectorsIndexExists = await client.asCurrentUser.indices.exists({ - index: CONNECTORS_INDEX, - }); - if (!connectorsIndexExists) { - await setupConnectorsIndices(client.asCurrentUser); - } return await createConnector(document, client, input.language, !!input.delete_existing_connector); }; diff --git a/x-pack/plugins/enterprise_search/server/lib/indices/generate_api_key.test.ts b/x-pack/plugins/enterprise_search/server/lib/indices/generate_api_key.test.ts index 0ff9800716cfe3..6d49e2e53cfbad 100644 --- a/x-pack/plugins/enterprise_search/server/lib/indices/generate_api_key.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/indices/generate_api_key.test.ts @@ -56,7 +56,7 @@ describe('generateApiKey lib function', () => { name: 'index_name-connector', role_descriptors: { ['index-name-connector-role']: { - cluster: [], + cluster: ['monitor/main'], index: [ { names: ['index_name', `${CONNECTORS_INDEX}*`], @@ -91,7 +91,7 @@ describe('generateApiKey lib function', () => { name: 'index_name-connector', role_descriptors: { ['index-name-connector-role']: { - cluster: [], + cluster: ['monitor/main'], index: [ { names: ['index_name', `${CONNECTORS_INDEX}*`], @@ -138,7 +138,7 @@ describe('generateApiKey lib function', () => { name: 'index_name-connector', role_descriptors: { ['index-name-connector-role']: { - cluster: [], + cluster: ['monitor/main'], index: [ { names: ['index_name', `${CONNECTORS_INDEX}*`], diff --git a/x-pack/plugins/enterprise_search/server/lib/indices/generate_api_key.ts b/x-pack/plugins/enterprise_search/server/lib/indices/generate_api_key.ts index a6d3455f918974..f76fcf41fa2454 100644 --- a/x-pack/plugins/enterprise_search/server/lib/indices/generate_api_key.ts +++ b/x-pack/plugins/enterprise_search/server/lib/indices/generate_api_key.ts @@ -16,7 +16,7 @@ export const generateApiKey = async (client: IScopedClusterClient, indexName: st name: `${indexName}-connector`, role_descriptors: { [`${toAlphanumeric(indexName)}-connector-role`]: { - cluster: [], + cluster: ['monitor/main'], index: [ { names: [indexName, `${CONNECTORS_INDEX}*`], diff --git a/x-pack/plugins/enterprise_search/server/ui_settings.ts b/x-pack/plugins/enterprise_search/server/ui_settings.ts index 15241cc5fe890c..0497aa54d2eecd 100644 --- a/x-pack/plugins/enterprise_search/server/ui_settings.ts +++ b/x-pack/plugins/enterprise_search/server/ui_settings.ts @@ -9,19 +9,19 @@ import { schema } from '@kbn/config-schema'; import { UiSettingsParams } from '@kbn/core/types'; import { i18n } from '@kbn/i18n'; -import { enterpriseSearchFeatureId, enableIndexTransformsTab } from '../common/ui_settings_keys'; +import { enterpriseSearchFeatureId, enableIndexPipelinesTab } from '../common/ui_settings_keys'; /** * uiSettings definitions for Enterprise Search */ export const uiSettings: Record<string, UiSettingsParams<boolean>> = { - [enableIndexTransformsTab]: { + [enableIndexPipelinesTab]: { category: [enterpriseSearchFeatureId], - description: i18n.translate('xpack.enterpriseSearch.uiSettings.indexTransforms.description', { - defaultMessage: 'Enable the new index transforms tab in Enterprise Search.', + description: i18n.translate('xpack.enterpriseSearch.uiSettings.indexPipelines.description', { + defaultMessage: 'Enable the new index pipelines tab in Enterprise Search.', }), - name: i18n.translate('xpack.enterpriseSearch.uiSettings.indexTransforms.name', { - defaultMessage: 'Enable index transforms', + name: i18n.translate('xpack.enterpriseSearch.uiSettings.indexPipelines.name', { + defaultMessage: 'Enable index pipelines', }), requiresPageReload: false, schema: schema.boolean(), diff --git a/x-pack/plugins/enterprise_search/server/utils/create_pipeline_definitions.test.ts b/x-pack/plugins/enterprise_search/server/utils/create_pipeline_definitions.test.ts index 27208dbaed00e5..6961086edac1be 100644 --- a/x-pack/plugins/enterprise_search/server/utils/create_pipeline_definitions.test.ts +++ b/x-pack/plugins/enterprise_search/server/utils/create_pipeline_definitions.test.ts @@ -8,6 +8,7 @@ import { ElasticsearchClient } from '@kbn/core/server'; import { createIndexPipelineDefinitions } from './create_pipeline_definitions'; +import { formatMlPipelineBody } from './create_pipeline_definitions'; describe('createIndexPipelineDefinitions util function', () => { const indexName = 'my-index'; @@ -34,3 +35,163 @@ describe('createIndexPipelineDefinitions util function', () => { expect(mockClient.ingest.putPipeline).toHaveBeenCalledTimes(3); }); }); + +describe('formatMlPipelineBody util function', () => { + const modelId = 'my-model-id'; + let modelInputField = 'my-model-input-field'; + const modelType = 'my-model-type'; + const modelVersion = 3; + const sourceField = 'my-source-field'; + const destField = 'my-dest-field'; + + const mockClient = { + ml: { + getTrainedModels: jest.fn(), + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return the pipeline body', async () => { + const expectedResult = { + description: '', + version: 1, + processors: [ + { + remove: { + field: `ml.inference.${destField}`, + ignore_missing: true, + }, + }, + { + inference: { + model_id: modelId, + target_field: `ml.inference.${destField}`, + field_map: { + sourceField: modelInputField, + }, + }, + }, + { + append: { + field: '_source._ingest.processors', + value: [ + { + type: modelType, + model_id: modelId, + model_version: modelVersion, + processed_timestamp: '{{{ _ingest.timestamp }}}', + }, + ], + }, + }, + ], + }; + + const mockResponse = { + count: 1, + trained_model_configs: [ + { + model_id: modelId, + version: modelVersion, + model_type: modelType, + input: { field_names: [modelInputField] }, + }, + ], + }; + mockClient.ml.getTrainedModels.mockImplementation(() => Promise.resolve(mockResponse)); + const actualResult = await formatMlPipelineBody( + modelId, + sourceField, + destField, + mockClient as unknown as ElasticsearchClient + ); + expect(actualResult).toEqual(expectedResult); + expect(mockClient.ml.getTrainedModels).toHaveBeenCalledTimes(1); + }); + + it('should raise an error if no model found', async () => { + const mockResponse = { + error: { + root_cause: [ + { + type: 'resource_not_found_exception', + reason: 'No known trained model with model_id [my-model-id]', + }, + ], + type: 'resource_not_found_exception', + reason: 'No known trained model with model_id [my-model-id]', + }, + status: 404, + }; + mockClient.ml.getTrainedModels.mockImplementation(() => Promise.resolve(mockResponse)); + const asyncCall = formatMlPipelineBody( + modelId, + sourceField, + destField, + mockClient as unknown as ElasticsearchClient + ); + await expect(asyncCall).rejects.toThrow(Error); + expect(mockClient.ml.getTrainedModels).toHaveBeenCalledTimes(1); + }); + + it('should insert a placeholder if model has no input fields', async () => { + modelInputField = 'MODEL_INPUT_FIELD'; + const expectedResult = { + description: '', + version: 1, + processors: [ + { + remove: { + field: `ml.inference.${destField}`, + ignore_missing: true, + }, + }, + { + inference: { + model_id: modelId, + target_field: `ml.inference.${destField}`, + field_map: { + sourceField: modelInputField, + }, + }, + }, + { + append: { + field: '_source._ingest.processors', + value: [ + { + type: modelType, + model_id: modelId, + model_version: modelVersion, + processed_timestamp: '{{{ _ingest.timestamp }}}', + }, + ], + }, + }, + ], + }; + const mockResponse = { + count: 1, + trained_model_configs: [ + { + model_id: modelId, + version: modelVersion, + model_type: modelType, + input: { field_names: [] }, + }, + ], + }; + mockClient.ml.getTrainedModels.mockImplementation(() => Promise.resolve(mockResponse)); + const actualResult = await formatMlPipelineBody( + modelId, + sourceField, + destField, + mockClient as unknown as ElasticsearchClient + ); + expect(actualResult).toEqual(expectedResult); + expect(mockClient.ml.getTrainedModels).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/utils/create_pipeline_definitions.ts b/x-pack/plugins/enterprise_search/server/utils/create_pipeline_definitions.ts index 377f12fd632087..666588dd098869 100644 --- a/x-pack/plugins/enterprise_search/server/utils/create_pipeline_definitions.ts +++ b/x-pack/plugins/enterprise_search/server/utils/create_pipeline_definitions.ts @@ -5,12 +5,17 @@ * 2.0. */ +import { IngestPipeline } from '@elastic/elasticsearch/lib/api/types'; import { ElasticsearchClient } from '@kbn/core/server'; export interface CreatedPipelines { created: string[]; } +export interface MlInferencePipeline extends IngestPipeline { + version?: number; +} + /** * Used to create index-specific Ingest Pipelines to be used in conjunction with Enterprise Search * ingestion mechanisms. Three pipelines are created: @@ -225,3 +230,64 @@ export const createIndexPipelineDefinitions = ( }); return { created: [indexName, `${indexName}@custom`, `${indexName}@ml-inference`] }; }; + +/** + * Format the body of an ML inference pipeline for a specified model. + * Does not create the pipeline, only returns JSON for the user to preview. + * @param modelId modelId selected by user. + * @param sourceField The document field that model will read. + * @param destinationField The document field that the model will write to. + * @param esClient the Elasticsearch Client to use when retrieving model details. + */ +export const formatMlPipelineBody = async ( + modelId: string, + sourceField: string, + destinationField: string, + esClient: ElasticsearchClient +): Promise<MlInferencePipeline> => { + const models = await esClient.ml.getTrainedModels({ model_id: modelId }); + // if we didn't find this model, we can't return anything useful + if (models.trained_model_configs === undefined || models.trained_model_configs.length === 0) { + throw new Error(`Couldn't find any trained models with id [${modelId}]`); + } + const model = models.trained_model_configs[0]; + // if model returned no input field, insert a placeholder + const modelInputField = + model.input?.field_names?.length > 0 ? model.input.field_names[0] : 'MODEL_INPUT_FIELD'; + const modelType = model.model_type; + const modelVersion = model.version; + return { + description: '', + version: 1, + processors: [ + { + remove: { + field: `ml.inference.${destinationField}`, + ignore_missing: true, + }, + }, + { + inference: { + model_id: modelId, + target_field: `ml.inference.${destinationField}`, + field_map: { + sourceField: modelInputField, + }, + }, + }, + { + append: { + field: '_source._ingest.processors', + value: [ + { + type: modelType, + model_id: modelId, + model_version: modelVersion, + processed_timestamp: '{{{ _ingest.timestamp }}}', + }, + ], + }, + }, + ], + }; +}; diff --git a/x-pack/plugins/file_upload/public/components/geo_upload_form/geo_upload_form.tsx b/x-pack/plugins/file_upload/public/components/geo_upload_form/geo_upload_form.tsx index 43dde2580b66d5..05b6c6244810f3 100644 --- a/x-pack/plugins/file_upload/public/components/geo_upload_form/geo_upload_form.tsx +++ b/x-pack/plugins/file_upload/public/components/geo_upload_form/geo_upload_form.tsx @@ -6,7 +6,15 @@ */ import React, { ChangeEvent, Component } from 'react'; -import { EuiForm, EuiFormRow, EuiSelect } from '@elastic/eui'; +import { + EuiForm, + EuiFormRow, + EuiSpacer, + EuiSelect, + EuiSwitch, + EuiSwitchEvent, + EuiToolTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ES_FIELD_TYPES } from '@kbn/data-plugin/public'; import { GeoFilePicker, OnFileSelectParameters } from './geo_file_picker'; @@ -28,12 +36,14 @@ interface Props { geoFieldType: ES_FIELD_TYPES.GEO_POINT | ES_FIELD_TYPES.GEO_SHAPE; indexName: string; indexNameError?: string; + smallChunks: boolean; onFileClear: () => void; onFileSelect: (onFileSelectParameters: OnFileSelectParameters) => void; onGeoFieldTypeSelect: (geoFieldType: ES_FIELD_TYPES.GEO_POINT | ES_FIELD_TYPES.GEO_SHAPE) => void; onIndexNameChange: (name: string, error?: string) => void; onIndexNameValidationStart: () => void; onIndexNameValidationEnd: () => void; + onSmallChunksChange: (smallChunks: boolean) => void; } interface State { @@ -96,6 +106,10 @@ export class GeoUploadForm extends Component<Props, State> { ); }; + _onSmallChunksChange = (event: EuiSwitchEvent) => { + this.props.onSmallChunksChange(event.target.checked); + }; + _renderGeoFieldTypeSelect() { return this.state.hasFile && this.state.isPointsOnly ? ( <EuiFormRow @@ -119,13 +133,33 @@ export class GeoUploadForm extends Component<Props, State> { <GeoFilePicker onSelect={this._onFileSelect} onClear={this._onFileClear} /> {this._renderGeoFieldTypeSelect()} {this.state.hasFile ? ( - <IndexNameForm - indexName={this.props.indexName} - indexNameError={this.props.indexNameError} - onIndexNameChange={this.props.onIndexNameChange} - onIndexNameValidationStart={this.props.onIndexNameValidationStart} - onIndexNameValidationEnd={this.props.onIndexNameValidationEnd} - /> + <> + <IndexNameForm + indexName={this.props.indexName} + indexNameError={this.props.indexNameError} + onIndexNameChange={this.props.onIndexNameChange} + onIndexNameValidationStart={this.props.onIndexNameValidationStart} + onIndexNameValidationEnd={this.props.onIndexNameValidationEnd} + /> + <EuiSpacer size="m" /> + <EuiFormRow display="columnCompressedSwitch"> + <EuiToolTip + position="top" + content={i18n.translate('xpack.fileUpload.smallChunks.tooltip', { + defaultMessage: 'Use to alleviate request timeout failures.', + })} + > + <EuiSwitch + label={i18n.translate('xpack.fileUpload.smallChunks.switchLabel', { + defaultMessage: 'Upload file in smaller chunks', + })} + checked={this.props.smallChunks} + onChange={this._onSmallChunksChange} + compressed + /> + </EuiToolTip> + </EuiFormRow> + </> ) : null} </EuiForm> ); diff --git a/x-pack/plugins/file_upload/public/components/geo_upload_wizard.tsx b/x-pack/plugins/file_upload/public/components/geo_upload_wizard.tsx index adbce777a49425..0c7f09c56f36f4 100644 --- a/x-pack/plugins/file_upload/public/components/geo_upload_wizard.tsx +++ b/x-pack/plugins/file_upload/public/components/geo_upload_wizard.tsx @@ -40,6 +40,7 @@ interface State { indexNameError?: string; dataViewResp?: object; phase: PHASE; + smallChunks: boolean; } export class GeoUploadWizard extends Component<FileUploadComponentProps, State> { @@ -52,6 +53,7 @@ export class GeoUploadWizard extends Component<FileUploadComponentProps, State> importStatus: '', indexName: '', phase: PHASE.CONFIGURE, + smallChunks: false, }; componentDidMount() { @@ -146,6 +148,7 @@ export class GeoUploadWizard extends Component<FileUploadComponentProps, State> this.setState({ importStatus: getWritingToIndexMsg(0), }); + this._geoFileImporter.setSmallChunks(this.state.smallChunks); const importResults = await this._geoFileImporter.import( initializeImportResp.id, this.state.indexName, @@ -281,6 +284,10 @@ export class GeoUploadWizard extends Component<FileUploadComponentProps, State> } }; + _onSmallChunksChange = (smallChunks: boolean) => { + this.setState({ smallChunks }); + }; + render() { if (this.state.phase === PHASE.IMPORT) { return ( @@ -311,10 +318,12 @@ export class GeoUploadWizard extends Component<FileUploadComponentProps, State> indexNameError={this.state.indexNameError} onFileClear={this._onFileClear} onFileSelect={this._onFileSelect} + smallChunks={this.state.smallChunks} onGeoFieldTypeSelect={this._onGeoFieldTypeSelect} onIndexNameChange={this._onIndexNameChange} onIndexNameValidationStart={this.props.disableImportBtn} onIndexNameValidationEnd={this.props.enableImportBtn} + onSmallChunksChange={this._onSmallChunksChange} /> ); } diff --git a/x-pack/plugins/file_upload/public/components/import_complete_view.tsx b/x-pack/plugins/file_upload/public/components/import_complete_view.tsx index 5ec4e6f0ddf375..46f566eb27e2e6 100644 --- a/x-pack/plugins/file_upload/public/components/import_complete_view.tsx +++ b/x-pack/plugins/file_upload/public/components/import_complete_view.tsx @@ -128,13 +128,20 @@ export class ImportCompleteView extends Component<Props, {}> { } if (!this.props.importResults || !this.props.importResults.success) { - const errorMsg = - this.props.importResults && this.props.importResults.error - ? i18n.translate('xpack.fileUpload.importComplete.uploadFailureMsgErrorBlock', { - defaultMessage: 'Error: {reason}', - values: { reason: this.props.importResults.error.error.reason }, - }) - : ''; + let reason: string | undefined; + if (this.props.importResults?.error?.body?.message) { + // Display http request error message + reason = this.props.importResults.error.body.message; + } else if (this.props.importResults?.error?.error?.reason) { + // Display elasticxsearch request error message + reason = this.props.importResults.error.error.reason; + } + const errorMsg = reason + ? i18n.translate('xpack.fileUpload.importComplete.uploadFailureMsgErrorBlock', { + defaultMessage: 'Error: {reason}', + values: { reason }, + }) + : ''; return ( <EuiCallOut title={i18n.translate('xpack.fileUpload.importComplete.uploadFailureTitle', { diff --git a/x-pack/plugins/file_upload/public/importer/geo/abstract_geo_file_importer.tsx b/x-pack/plugins/file_upload/public/importer/geo/abstract_geo_file_importer.tsx index 31132e0d9698d5..afc95cc8307683 100644 --- a/x-pack/plugins/file_upload/public/importer/geo/abstract_geo_file_importer.tsx +++ b/x-pack/plugins/file_upload/public/importer/geo/abstract_geo_file_importer.tsx @@ -34,6 +34,7 @@ export class AbstractGeoFileImporter extends Importer implements GeoFileImporter private _invalidFeatures: ImportFailure[] = []; private _geoFieldType: ES_FIELD_TYPES.GEO_POINT | ES_FIELD_TYPES.GEO_SHAPE = ES_FIELD_TYPES.GEO_SHAPE; + private _smallChunks = false; constructor(file: File) { super(); @@ -74,6 +75,10 @@ export class AbstractGeoFileImporter extends Importer implements GeoFileImporter this._geoFieldType = geoFieldType; } + public setSmallChunks(smallChunks: boolean) { + this._smallChunks = smallChunks; + } + public async import( id: string, index: string, @@ -89,6 +94,7 @@ export class AbstractGeoFileImporter extends Importer implements GeoFileImporter }; } + const maxChunkCharCount = this._smallChunks ? MAX_CHUNK_CHAR_COUNT / 10 : MAX_CHUNK_CHAR_COUNT; let success = true; const failures: ImportFailure[] = [...this._invalidFeatures]; let error; @@ -120,7 +126,7 @@ export class AbstractGeoFileImporter extends Importer implements GeoFileImporter } // Import block in chunks to avoid sending too much data to Elasticsearch at a time. - const chunks = createChunks(this._features, this._geoFieldType, MAX_CHUNK_CHAR_COUNT); + const chunks = createChunks(this._features, this._geoFieldType, maxChunkCharCount); const blockSizeInBytes = this._blockSizeInBytes; // reset block for next read diff --git a/x-pack/plugins/file_upload/public/importer/geo/types.ts b/x-pack/plugins/file_upload/public/importer/geo/types.ts index 3c9115f1c27ac7..1fbd5e3c515b79 100644 --- a/x-pack/plugins/file_upload/public/importer/geo/types.ts +++ b/x-pack/plugins/file_upload/public/importer/geo/types.ts @@ -23,4 +23,5 @@ export interface GeoFileImporter extends IImporter { previewFile(rowLimit?: number, sizeLimit?: number): Promise<GeoFilePreview>; renderEditor(onChange: () => void): ReactNode; setGeoFieldType(geoFieldType: ES_FIELD_TYPES.GEO_POINT | ES_FIELD_TYPES.GEO_SHAPE): void; + setSmallChunks(smallChunks: boolean): void; } diff --git a/x-pack/plugins/files/server/routes/common.test.ts b/x-pack/plugins/files/server/routes/common.test.ts index a8a1a5403c8914..2c4d302d04625a 100644 --- a/x-pack/plugins/files/server/routes/common.test.ts +++ b/x-pack/plugins/files/server/routes/common.test.ts @@ -20,6 +20,7 @@ describe('getDownloadHeadersForFile', () => { 'content-type': contentType, 'content-disposition': `attachment; filename="${contentDisposition}"`, 'cache-control': 'max-age=31536000, immutable', + 'x-content-type-options': 'nosniff', }; } diff --git a/x-pack/plugins/files/server/routes/common.ts b/x-pack/plugins/files/server/routes/common.ts index 8bfc7753efe3f4..0730a6435de028 100644 --- a/x-pack/plugins/files/server/routes/common.ts +++ b/x-pack/plugins/files/server/routes/common.ts @@ -15,6 +15,8 @@ export function getDownloadHeadersForFile(file: File, fileName?: string): Respon // Note, this name can be overridden by the client if set via a "download" attribute on the HTML tag. 'content-disposition': `attachment; filename="${fileName || getDownloadedFileName(file)}"`, 'cache-control': 'max-age=31536000, immutable', + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options + 'x-content-type-options': 'nosniff', }; } diff --git a/x-pack/plugins/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts index b415328a303d3c..fa9e074899163a 100644 --- a/x-pack/plugins/fleet/common/constants/routes.ts +++ b/x-pack/plugins/fleet/common/constants/routes.ts @@ -126,6 +126,7 @@ export const AGENT_API_ROUTES = { UPGRADE_PATTERN: `${API_ROOT}/agents/{agentId}/upgrade`, BULK_UPGRADE_PATTERN: `${API_ROOT}/agents/bulk_upgrade`, CURRENT_UPGRADES_PATTERN: `${API_ROOT}/agents/current_upgrades`, + ACTION_STATUS_PATTERN: `${API_ROOT}/agents/action_status`, LIST_TAGS_PATTERN: `${API_ROOT}/agents/tags`, }; diff --git a/x-pack/plugins/fleet/common/services/__snapshots__/package_to_package_policy.test.ts.snap b/x-pack/plugins/fleet/common/services/__snapshots__/package_to_package_policy.test.ts.snap index 3235ebbd9a8f95..1edc3d850f7756 100644 --- a/x-pack/plugins/fleet/common/services/__snapshots__/package_to_package_policy.test.ts.snap +++ b/x-pack/plugins/fleet/common/services/__snapshots__/package_to_package_policy.test.ts.snap @@ -15,6 +15,7 @@ Object { "type": "metrics", }, "enabled": true, + "release": "beta", "vars": Object { "cost_explorer_config.group_by_dimension_keys": Object { "type": "text", @@ -53,6 +54,7 @@ Object { "type": "logs", }, "enabled": true, + "release": "beta", "vars": Object { "api_timeout": Object { "type": "text", @@ -85,6 +87,7 @@ Object { "type": "logs", }, "enabled": false, + "release": "beta", "vars": Object { "interval": Object { "type": "text", @@ -131,6 +134,7 @@ Object { "type": "logs", }, "enabled": true, + "release": "beta", "vars": Object { "api_timeout": Object { "type": "text", @@ -163,6 +167,7 @@ Object { "type": "metrics", }, "enabled": true, + "release": "beta", "vars": Object { "latency": Object { "type": "text", @@ -209,6 +214,7 @@ Object { "type": "metrics", }, "enabled": true, + "release": "beta", "vars": Object { "latency": Object { "type": "text", @@ -243,6 +249,7 @@ Object { "type": "metrics", }, "enabled": true, + "release": "beta", "vars": Object { "latency": Object { "type": "text", @@ -277,6 +284,7 @@ Object { "type": "logs", }, "enabled": true, + "release": "beta", "vars": Object { "api_timeout": Object { "type": "text", @@ -309,6 +317,7 @@ Object { "type": "metrics", }, "enabled": true, + "release": "beta", "vars": Object { "latency": Object { "type": "text", @@ -343,6 +352,7 @@ Object { "type": "logs", }, "enabled": true, + "release": "beta", "vars": Object { "api_timeout": Object { "type": "text", @@ -375,6 +385,7 @@ Object { "type": "metrics", }, "enabled": true, + "release": "beta", "vars": Object { "latency": Object { "type": "text", @@ -409,6 +420,7 @@ Object { "type": "metrics", }, "enabled": true, + "release": "beta", "vars": Object { "latency": Object { "type": "text", @@ -443,6 +455,7 @@ Object { "type": "metrics", }, "enabled": true, + "release": "beta", "vars": Object { "latency": Object { "type": "text", @@ -471,6 +484,7 @@ Object { "type": "metrics", }, "enabled": true, + "release": "beta", "vars": Object { "latency": Object { "type": "text", @@ -505,6 +519,7 @@ Object { "type": "logs", }, "enabled": true, + "release": "beta", "vars": Object { "api_timeout": Object { "type": "text", @@ -537,6 +552,7 @@ Object { "type": "metrics", }, "enabled": true, + "release": "beta", "vars": Object { "latency": Object { "type": "text", @@ -558,6 +574,7 @@ Object { "type": "metrics", }, "enabled": true, + "release": "beta", "vars": Object { "latency": Object { "type": "text", @@ -586,6 +603,7 @@ Object { "type": "metrics", }, "enabled": true, + "release": "beta", "vars": Object { "latency": Object { "type": "text", @@ -620,6 +638,7 @@ Object { "type": "metrics", }, "enabled": true, + "release": "beta", "vars": Object { "latency": Object { "type": "text", @@ -648,6 +667,7 @@ Object { "type": "metrics", }, "enabled": true, + "release": "beta", "vars": Object { "latency": Object { "type": "text", @@ -676,6 +696,7 @@ Object { "type": "metrics", }, "enabled": true, + "release": "beta", "vars": Object { "latency": Object { "type": "text", @@ -704,6 +725,7 @@ Object { "type": "logs", }, "enabled": true, + "release": "beta", "vars": Object { "api_timeout": Object { "type": "text", @@ -736,6 +758,7 @@ Object { "type": "metrics", }, "enabled": true, + "release": "beta", "vars": Object { "latency": Object { "type": "text", diff --git a/x-pack/plugins/fleet/common/services/package_to_package_policy.ts b/x-pack/plugins/fleet/common/services/package_to_package_policy.ts index cea02893e849d3..6944df380d3dd2 100644 --- a/x-pack/plugins/fleet/common/services/package_to_package_policy.ts +++ b/x-pack/plugins/fleet/common/services/package_to_package_policy.ts @@ -19,12 +19,16 @@ import type { import { doesPackageHaveIntegrations } from '.'; +type PackagePolicyStream = RegistryStream & { release?: 'beta' | 'experimental' | 'ga' } & { + data_stream: { type: string; dataset: string }; +}; + export const getStreamsForInputType = ( inputType: string, packageInfo: PackageInfo, dataStreamPaths: string[] = [] -): Array<RegistryStream & { data_stream: { type: string; dataset: string } }> => { - const streams: Array<RegistryStream & { data_stream: { type: string; dataset: string } }> = []; +): PackagePolicyStream[] => { + const streams: PackagePolicyStream[] = []; const dataStreams = packageInfo.data_streams || []; const dataStreamsToSearch = dataStreamPaths.length ? dataStreams.filter((dataStream) => dataStreamPaths.includes(dataStream.path)) @@ -39,6 +43,7 @@ export const getStreamsForInputType = ( type: dataStream.type, dataset: dataStream.dataset, }, + release: dataStream.release, }); } }); @@ -102,6 +107,7 @@ export const packageToPackagePolicyInputs = ( const stream: NewPackagePolicyInputStream = { enabled: packageStream.enabled === false ? false : true, data_stream: packageStream.data_stream, + release: packageStream.release, }; if (packageStream.vars && packageStream.vars.length) { stream.vars = packageStream.vars.reduce(varsReducer, {}); diff --git a/x-pack/plugins/fleet/common/services/routes.ts b/x-pack/plugins/fleet/common/services/routes.ts index 323d7d1f8b3786..c2f76758c3d7bd 100644 --- a/x-pack/plugins/fleet/common/services/routes.ts +++ b/x-pack/plugins/fleet/common/services/routes.ts @@ -194,6 +194,7 @@ export const agentRouteService = { getUpgradePath: (agentId: string) => AGENT_API_ROUTES.UPGRADE_PATTERN.replace('{agentId}', agentId), getBulkUpgradePath: () => AGENT_API_ROUTES.BULK_UPGRADE_PATTERN, + getActionStatusPath: () => AGENT_API_ROUTES.ACTION_STATUS_PATTERN, getCurrentUpgradesPath: () => AGENT_API_ROUTES.CURRENT_UPGRADES_PATTERN, getCancelActionPath: (actionId: string) => AGENT_API_ROUTES.CANCEL_ACTIONS_PATTERN.replace('{actionId}', actionId), diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index 9924413cb16bf4..0bc0ddb22d150c 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -47,6 +47,7 @@ export interface NewAgentAction { start_time?: string; minimum_execution_duration?: number; source_uri?: string; + total?: number; } export interface AgentAction extends NewAgentAction { @@ -80,6 +81,7 @@ interface AgentBase { user_provided_metadata: AgentMetadata; local_metadata: AgentMetadata; tags?: string[]; + components?: FleetServerAgentComponent[]; } export interface Agent extends AgentBase { @@ -104,7 +106,23 @@ export interface CurrentUpgrade { startTime?: string; } -interface FleetServerAgentComponentUnit { +export interface ActionStatus { + actionId: string; + // how many agents are successfully included in action documents + nbAgentsActionCreated: number; + // how many agents acknowledged the action sucessfully (completed) + nbAgentsAck: number; + version: string; + startTime?: string; + type?: string; + // how many agents were actioned by the user + nbAgentsActioned: number; + status: 'complete' | 'expired' | 'cancelled' | 'failed' | 'in progress'; + errorMessage?: string; +} + +// Generated from FleetServer schema.json +export interface FleetServerAgentComponentUnit { id: string; type: 'input' | 'output'; status: FleetServerAgentComponentStatus; @@ -122,8 +140,6 @@ interface FleetServerAgentComponent { units: FleetServerAgentComponentUnit[]; } -// Initially generated from FleetServer schema.json - /** * An Elastic Agent that has enrolled into Fleet */ @@ -309,5 +325,7 @@ export interface FleetServerAgentAction { data?: { [k: string]: unknown; }; + + total?: number; [k: string]: unknown; } diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 8c8e6288d474ed..977d9d2be7d618 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -315,7 +315,7 @@ export interface RegistryDataStream { [RegistryDataStreamKeys.hidden]?: boolean; [RegistryDataStreamKeys.dataset]: string; [RegistryDataStreamKeys.title]: string; - [RegistryDataStreamKeys.release]: string; + [RegistryDataStreamKeys.release]: RegistryRelease; [RegistryDataStreamKeys.streams]?: RegistryStream[]; [RegistryDataStreamKeys.package]: string; [RegistryDataStreamKeys.path]: string; @@ -417,6 +417,14 @@ export type PackageInfo = | Installable<Merge<RegistryPackage, EpmPackageAdditions>> | Installable<Merge<ArchivePackage, EpmPackageAdditions>>; +// TODO - Expand this with other experimental indexing types +export type ExperimentalIndexingFeature = 'synthetic_source'; + +export interface ExperimentalDataStreamFeature { + data_stream: string; + features: Record<ExperimentalIndexingFeature, boolean>; +} + export interface Installation extends SavedObjectAttributes { installed_kibana: KibanaAssetReference[]; installed_es: EsAssetReference[]; @@ -433,6 +441,11 @@ export interface Installation extends SavedObjectAttributes { install_format_schema_version?: string; verification_status: PackageVerificationStatus; verification_key_id?: string | null; + // TypeScript doesn't like using the `ExperimentalDataStreamFeature` type defined above here + experimental_data_stream_features?: Array<{ + data_stream: string; + features: Record<ExperimentalIndexingFeature, boolean>; + }>; } export interface PackageUsageStats { diff --git a/x-pack/plugins/fleet/common/types/models/package_policy.ts b/x-pack/plugins/fleet/common/types/models/package_policy.ts index ec6b04ae64fc8e..629250c243cea8 100644 --- a/x-pack/plugins/fleet/common/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/package_policy.ts @@ -5,10 +5,13 @@ * 2.0. */ +import type { RegistryRelease, ExperimentalDataStreamFeature } from './epm'; + export interface PackagePolicyPackage { name: string; title: string; version: string; + experimental_data_stream_features?: ExperimentalDataStreamFeature[]; } export interface PackagePolicyConfigRecordEntry { @@ -32,6 +35,7 @@ export interface NewPackagePolicyInputStream { }; }; }; + release?: RegistryRelease; vars?: PackagePolicyConfigRecord; config?: PackagePolicyConfigRecord; } diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts index dae156c8c83548..7050fcd3da3466 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts @@ -7,7 +7,7 @@ import type { SearchHit } from '@kbn/core/types/elasticsearch'; -import type { Agent, AgentAction, CurrentUpgrade, NewAgentAction } from '../models'; +import type { Agent, AgentAction, ActionStatus, CurrentUpgrade, NewAgentAction } from '../models'; import type { ListResult, ListWithKuery } from './common'; @@ -125,6 +125,7 @@ export interface PostBulkAgentReassignRequest { body: { policy_id: string; agents: string[] | string; + batchSize?: number; }; } @@ -205,6 +206,9 @@ export interface GetAgentIncomingDataResponse { export interface GetCurrentUpgradesResponse { items: CurrentUpgrade[]; } +export interface GetActionStatusResponse { + items: ActionStatus[]; +} export interface GetAvailableVersionsResponse { items: string[]; } diff --git a/x-pack/plugins/fleet/cypress.config.ts b/x-pack/plugins/fleet/cypress.config.ts new file mode 100644 index 00000000000000..e2d5ffd3ffdac8 --- /dev/null +++ b/x-pack/plugins/fleet/cypress.config.ts @@ -0,0 +1,44 @@ +/* + * 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. + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import { defineConfig } from 'cypress'; + +// eslint-disable-next-line import/no-default-export +export default defineConfig({ + defaultCommandTimeout: 60000, + requestTimeout: 60000, + responseTimeout: 60000, + execTimeout: 120000, + pageLoadTimeout: 120000, + + retries: { + runMode: 2, + }, + + screenshotsFolder: '../../../target/kibana-fleet/cypress/screenshots', + trashAssetsBeforeRuns: false, + video: false, + videosFolder: '../../../target/kibana-fleet/cypress/videos', + viewportHeight: 900, + viewportWidth: 1440, + screenshotOnRunFailure: true, + + env: { + protocol: 'http', + hostname: 'localhost', + configport: '5601', + }, + + e2e: { + baseUrl: 'http://localhost:5601', + setupNodeEvents(on, config) { + // eslint-disable-next-line @typescript-eslint/no-var-requires, @kbn/imports/no_boundary_crossing + return require('./cypress/plugins')(on, config); + }, + }, +}); diff --git a/x-pack/plugins/fleet/cypress/cypress.json b/x-pack/plugins/fleet/cypress/cypress.json deleted file mode 100644 index b36d0c513116ce..00000000000000 --- a/x-pack/plugins/fleet/cypress/cypress.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "baseUrl": "http://localhost:5620", - "defaultCommandTimeout": 60000, - "requestTimeout": 60000, - "responseTimeout": 60000, - "execTimeout": 120000, - "pageLoadTimeout": 120000, - "nodeVersion": "system", - "retries": { - "runMode": 2 - }, - "screenshotsFolder": "../../../target/kibana-fleet/cypress/screenshots", - "trashAssetsBeforeRuns": false, - "video": false, - "videosFolder": "../../../target/kibana-fleet/cypress/videos", - "viewportHeight": 900, - "viewportWidth": 1440, - "screenshotOnRunFailure": true, - "env": { - "protocol": "http", - "hostname": "localhost", - "configport": "5601" - } -} diff --git a/x-pack/plugins/fleet/cypress/downloads/downloads.html b/x-pack/plugins/fleet/cypress/downloads/downloads.html deleted file mode 100644 index 772778ea352e58..00000000000000 Binary files a/x-pack/plugins/fleet/cypress/downloads/downloads.html and /dev/null differ diff --git a/x-pack/plugins/fleet/cypress/integration/a11y/home_page.spec.ts b/x-pack/plugins/fleet/cypress/e2e/a11y/home_page.cy.ts similarity index 98% rename from x-pack/plugins/fleet/cypress/integration/a11y/home_page.spec.ts rename to x-pack/plugins/fleet/cypress/e2e/a11y/home_page.cy.ts index b5d6e9d605f1e4..b76942ec9a456e 100644 --- a/x-pack/plugins/fleet/cypress/integration/a11y/home_page.spec.ts +++ b/x-pack/plugins/fleet/cypress/e2e/a11y/home_page.cy.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -/* eslint-disable-next-line import/no-extraneous-dependencies */ + import 'cypress-real-events/support'; import { checkA11y } from '../../support/commands'; import { FLEET, navigateTo } from '../../tasks/navigation'; diff --git a/x-pack/plugins/fleet/cypress/integration/agent_binary_download_source.spec.ts b/x-pack/plugins/fleet/cypress/e2e/agent_binary_download_source.cy.ts similarity index 100% rename from x-pack/plugins/fleet/cypress/integration/agent_binary_download_source.spec.ts rename to x-pack/plugins/fleet/cypress/e2e/agent_binary_download_source.cy.ts diff --git a/x-pack/plugins/fleet/cypress/integration/agent_list.spec.ts b/x-pack/plugins/fleet/cypress/e2e/agent_list.cy.ts similarity index 100% rename from x-pack/plugins/fleet/cypress/integration/agent_list.spec.ts rename to x-pack/plugins/fleet/cypress/e2e/agent_list.cy.ts diff --git a/x-pack/plugins/fleet/cypress/integration/agent_policy.spec.ts b/x-pack/plugins/fleet/cypress/e2e/agent_policy.cy.ts similarity index 100% rename from x-pack/plugins/fleet/cypress/integration/agent_policy.spec.ts rename to x-pack/plugins/fleet/cypress/e2e/agent_policy.cy.ts diff --git a/x-pack/plugins/fleet/cypress/integration/enrollment_token.spec.ts b/x-pack/plugins/fleet/cypress/e2e/enrollment_token.cy.ts similarity index 100% rename from x-pack/plugins/fleet/cypress/integration/enrollment_token.spec.ts rename to x-pack/plugins/fleet/cypress/e2e/enrollment_token.cy.ts diff --git a/x-pack/plugins/fleet/cypress/integration/fleet_agent_flyout.spec.ts b/x-pack/plugins/fleet/cypress/e2e/fleet_agent_flyout.cy.ts similarity index 100% rename from x-pack/plugins/fleet/cypress/integration/fleet_agent_flyout.spec.ts rename to x-pack/plugins/fleet/cypress/e2e/fleet_agent_flyout.cy.ts diff --git a/x-pack/plugins/fleet/cypress/integration/fleet_settings.spec.ts b/x-pack/plugins/fleet/cypress/e2e/fleet_settings.cy.ts similarity index 100% rename from x-pack/plugins/fleet/cypress/integration/fleet_settings.spec.ts rename to x-pack/plugins/fleet/cypress/e2e/fleet_settings.cy.ts diff --git a/x-pack/plugins/fleet/cypress/integration/fleet_startup.spec.ts b/x-pack/plugins/fleet/cypress/e2e/fleet_startup.cy.ts similarity index 100% rename from x-pack/plugins/fleet/cypress/integration/fleet_startup.spec.ts rename to x-pack/plugins/fleet/cypress/e2e/fleet_startup.cy.ts diff --git a/x-pack/plugins/fleet/cypress/integration/install_assets.spec.ts b/x-pack/plugins/fleet/cypress/e2e/install_assets.cy.ts similarity index 100% rename from x-pack/plugins/fleet/cypress/integration/install_assets.spec.ts rename to x-pack/plugins/fleet/cypress/e2e/install_assets.cy.ts diff --git a/x-pack/plugins/fleet/cypress/integration/integrations_mock.spec.ts b/x-pack/plugins/fleet/cypress/e2e/integrations_mock.cy.ts similarity index 100% rename from x-pack/plugins/fleet/cypress/integration/integrations_mock.spec.ts rename to x-pack/plugins/fleet/cypress/e2e/integrations_mock.cy.ts diff --git a/x-pack/plugins/fleet/cypress/integration/integrations_real.spec.ts b/x-pack/plugins/fleet/cypress/e2e/integrations_real.cy.ts similarity index 70% rename from x-pack/plugins/fleet/cypress/integration/integrations_real.spec.ts rename to x-pack/plugins/fleet/cypress/e2e/integrations_real.cy.ts index 34fa5b7af55ca5..c3bee2d758df0b 100644 --- a/x-pack/plugins/fleet/cypress/integration/integrations_real.spec.ts +++ b/x-pack/plugins/fleet/cypress/e2e/integrations_real.cy.ts @@ -22,14 +22,49 @@ import { POLICIES_TAB, SETTINGS_TAB, UPDATE_PACKAGE_BTN, - INTEGRATIONS_SEARCHBAR_INPUT, + INTEGRATIONS_SEARCHBAR, SETTINGS, INTEGRATION_POLICIES_UPGRADE_CHECKBOX, + INTEGRATION_LIST, + getIntegrationCategories, } from '../screens/integrations'; import { LOADING_SPINNER, CONFIRM_MODAL } from '../screens/navigation'; import { ADD_PACKAGE_POLICY_BTN } from '../screens/fleet'; import { cleanupAgentPolicies } from '../tasks/cleanup'; +function setupIntegrations() { + cy.intercept( + '/api/fleet/epm/packages?*', + { + middleware: true, + }, + (req) => { + req.on('before:response', (res) => { + // force all API responses to not be cached + res.headers['cache-control'] = 'no-store'; + }); + } + ).as('packages'); + + navigateTo(INTEGRATIONS); + cy.wait('@packages'); +} + +it('should install integration without policy', () => { + cy.visit('/app/integrations/detail/tomcat/settings'); + + cy.getBySel(SETTINGS.INSTALL_ASSETS_BTN).click(); + cy.get('.euiCallOut').contains('This action will install 1 assets'); + cy.getBySel(CONFIRM_MODAL.CONFIRM_BUTTON).click(); + + cy.getBySel(LOADING_SPINNER).should('not.exist'); + + cy.getBySel(SETTINGS.UNINSTALL_ASSETS_BTN).click(); + cy.getBySel(CONFIRM_MODAL.CONFIRM_BUTTON).click(); + cy.getBySel(LOADING_SPINNER).should('not.exist'); + cy.getBySel(SETTINGS.INSTALL_ASSETS_BTN).should('exist'); +}); + describe('Add Integration - Real API', () => { const integration = 'apache'; @@ -41,29 +76,6 @@ describe('Add Integration - Real API', () => { cleanupAgentPolicies(); }); - function addAndVerifyIntegration() { - cy.intercept( - '/api/fleet/epm/packages?*', - { - middleware: true, - }, - (req) => { - req.on('before:response', (res) => { - // force all API responses to not be cached - res.headers['cache-control'] = 'no-store'; - }); - } - ).as('packages'); - - navigateTo(INTEGRATIONS); - cy.wait('@packages'); - cy.getBySel(LOADING_SPINNER).should('not.exist'); - cy.getBySel(INTEGRATIONS_SEARCHBAR_INPUT).type('Apache'); - cy.getBySel(getIntegrationCard(integration)).click(); - addIntegration(); - cy.getBySel(INTEGRATION_NAME_LINK).contains('apache-1'); - } - it('should install integration without policy', () => { cy.visit('/app/integrations/detail/tomcat/settings'); @@ -80,7 +92,12 @@ describe('Add Integration - Real API', () => { }); it('should display Apache integration in the Policies list once installed ', () => { - addAndVerifyIntegration(); + setupIntegrations(); + cy.getBySel(LOADING_SPINNER).should('not.exist'); + cy.getBySel(INTEGRATIONS_SEARCHBAR.INPUT).clear().type('Apache'); + cy.getBySel(getIntegrationCard(integration)).click(); + addIntegration(); + cy.getBySel(INTEGRATION_NAME_LINK).contains('apache-1'); cy.getBySel(AGENT_POLICY_NAME_LINK).contains('Agent policy 1'); }); @@ -118,7 +135,7 @@ describe('Add Integration - Real API', () => { cy.getBySel(ADD_PACKAGE_POLICY_BTN).click(); cy.wait('@packages'); cy.getBySel(LOADING_SPINNER).should('not.exist'); - cy.getBySel(INTEGRATIONS_SEARCHBAR_INPUT).type('Apache'); + cy.getBySel(INTEGRATIONS_SEARCHBAR.INPUT).clear().type('Apache'); cy.getBySel(getIntegrationCard(integration)).click(); addIntegration({ useExistingPolicy: true }); cy.get('.euiBasicTable-loading').should('not.exist'); @@ -152,4 +169,16 @@ describe('Add Integration - Real API', () => { cy.getBySel(PACKAGE_VERSION).contains(newVersion); }); }); + + it('should filter integrations by category', () => { + setupIntegrations(); + cy.getBySel(getIntegrationCategories('aws')).click(); + cy.getBySel(INTEGRATIONS_SEARCHBAR.BADGE).contains('AWS').should('exist'); + cy.getBySel(INTEGRATION_LIST).find('.euiCard').should('have.length', 30); + + cy.getBySel(INTEGRATIONS_SEARCHBAR.INPUT).clear().type('Cloud'); + cy.getBySel(INTEGRATION_LIST).find('.euiCard').should('have.length', 3); + cy.getBySel(INTEGRATIONS_SEARCHBAR.REMOVE_BADGE_BUTTON).click(); + cy.getBySel(INTEGRATIONS_SEARCHBAR.BADGE).should('not.exist'); + }); }); diff --git a/x-pack/plugins/fleet/cypress/integration/package_policy.spec.ts b/x-pack/plugins/fleet/cypress/e2e/package_policy.cy.ts similarity index 100% rename from x-pack/plugins/fleet/cypress/integration/package_policy.spec.ts rename to x-pack/plugins/fleet/cypress/e2e/package_policy.cy.ts diff --git a/x-pack/plugins/fleet/cypress/integration/privileges_fleet_all_integrations_none.spec.ts b/x-pack/plugins/fleet/cypress/e2e/privileges_fleet_all_integrations_none.cy.ts similarity index 100% rename from x-pack/plugins/fleet/cypress/integration/privileges_fleet_all_integrations_none.spec.ts rename to x-pack/plugins/fleet/cypress/e2e/privileges_fleet_all_integrations_none.cy.ts diff --git a/x-pack/plugins/fleet/cypress/integration/privileges_fleet_all_integrations_read.spec.ts b/x-pack/plugins/fleet/cypress/e2e/privileges_fleet_all_integrations_read.cy.ts similarity index 100% rename from x-pack/plugins/fleet/cypress/integration/privileges_fleet_all_integrations_read.spec.ts rename to x-pack/plugins/fleet/cypress/e2e/privileges_fleet_all_integrations_read.cy.ts diff --git a/x-pack/plugins/fleet/cypress/integration/privileges_fleet_none_integrations_all.spec.ts b/x-pack/plugins/fleet/cypress/e2e/privileges_fleet_none_integrations_all.cy.ts similarity index 100% rename from x-pack/plugins/fleet/cypress/integration/privileges_fleet_none_integrations_all.spec.ts rename to x-pack/plugins/fleet/cypress/e2e/privileges_fleet_none_integrations_all.cy.ts diff --git a/x-pack/plugins/fleet/cypress/screens/integrations.ts b/x-pack/plugins/fleet/cypress/screens/integrations.ts index 3915c6600baaa0..63b13d8ff7fd3a 100644 --- a/x-pack/plugins/fleet/cypress/screens/integrations.ts +++ b/x-pack/plugins/fleet/cypress/screens/integrations.ts @@ -23,7 +23,12 @@ export const LATEST_VERSION = 'epmSettings.latestVersionTitle'; export const INSTALLED_VERSION = 'epmSettings.installedVersionTitle'; export const PACKAGE_VERSION = 'packageVersionText'; -export const INTEGRATIONS_SEARCHBAR_INPUT = 'epmList.searchBar'; +export const INTEGRATION_LIST = 'epmList.integrationCards'; +export const INTEGRATIONS_SEARCHBAR = { + INPUT: 'epmList.searchBar', + BADGE: 'epmList.categoryBadge', + REMOVE_BADGE_BUTTON: 'epmList.categoryBadge.closeBtn', +}; export const SETTINGS = { INSTALL_ASSETS_BTN: 'installAssetsButton', @@ -33,3 +38,4 @@ export const SETTINGS = { export const INTEGRATION_POLICIES_UPGRADE_CHECKBOX = 'epmDetails.upgradePoliciesCheckbox'; export const getIntegrationCard = (integration: string) => `integration-card:epr:${integration}`; +export const getIntegrationCategories = (category: string) => `epmList.categories.${category}`; diff --git a/x-pack/plugins/fleet/cypress/support/index.ts b/x-pack/plugins/fleet/cypress/support/e2e.ts similarity index 100% rename from x-pack/plugins/fleet/cypress/support/index.ts rename to x-pack/plugins/fleet/cypress/support/e2e.ts diff --git a/x-pack/plugins/fleet/cypress/tsconfig.json b/x-pack/plugins/fleet/cypress/tsconfig.json index a2a0a7f8aabf2b..aba041b4e17b8b 100644 --- a/x-pack/plugins/fleet/cypress/tsconfig.json +++ b/x-pack/plugins/fleet/cypress/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "../../../../tsconfig.base.json", "include": [ - "**/*" + "**/*", + "../cypress.config.ts" ], "exclude": [ "target/**/*" diff --git a/x-pack/plugins/fleet/dev_docs/fleet_ui_extensions.md b/x-pack/plugins/fleet/dev_docs/fleet_ui_extensions.md index 32486a8db3df59..9dcb7112f7ef08 100644 --- a/x-pack/plugins/fleet/dev_docs/fleet_ui_extensions.md +++ b/x-pack/plugins/fleet/dev_docs/fleet_ui_extensions.md @@ -44,6 +44,12 @@ export class Plugin { view: 'package-policy-response', Component: getLazyEndpointPolicyResponseExtension(core, plugins), }); + + registerExtension({ + package: 'endpoint', + view: 'package-generic-errors-list', + Component: getLazyEndpointGenericErrorsListExtension(core, plugins), + }); } //... } diff --git a/x-pack/plugins/fleet/kibana.json b/x-pack/plugins/fleet/kibana.json index 462e68a0979622..6ab87283e0b26f 100644 --- a/x-pack/plugins/fleet/kibana.json +++ b/x-pack/plugins/fleet/kibana.json @@ -8,7 +8,7 @@ "server": true, "ui": true, "configPath": ["xpack", "fleet"], - "requiredPlugins": ["licensing", "data", "encryptedSavedObjects", "navigation", "customIntegrations", "share", "spaces", "security", "unifiedSearch", "savedObjectsTagging"], + "requiredPlugins": ["licensing", "data", "encryptedSavedObjects", "navigation", "customIntegrations", "share", "spaces", "security", "unifiedSearch", "savedObjectsTagging", "taskManager"], "optionalPlugins": ["features", "cloud", "usageCollection", "home", "globalSearch", "telemetry", "discover", "ingestPipelines"], "extraPublicDirs": ["common"], "requiredBundles": ["kibanaReact", "cloud", "esUiShared", "infra", "kibanaUtils", "usageCollection", "unifiedSearch"] diff --git a/x-pack/plugins/fleet/package.json b/x-pack/plugins/fleet/package.json index b1ff148358ee6d..9a1464fbc17092 100644 --- a/x-pack/plugins/fleet/package.json +++ b/x-pack/plugins/fleet/package.json @@ -6,11 +6,11 @@ "license": "Elastic-License", "scripts": { "cypress": "../../../node_modules/.bin/cypress", - "cypress:open": "yarn cypress open --config-file ./cypress/cypress.json", + "cypress:open": "yarn cypress open --config-file ./cypress.config.ts", "cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/fleet_cypress/visual_config.ts", - "cypress:run": "yarn cypress:run:reporter --browser chrome --spec './cypress/integration/**/*.spec.ts'; status=$?; yarn junit:merge && exit $status", + "cypress:run": "yarn cypress:run:reporter --browser chrome --spec './cypress/e2e/**/*.cy.ts'; status=$?; yarn junit:merge && exit $status", "cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/fleet_cypress/cli_config.ts", - "cypress:run:reporter": "yarn cypress run --config-file ./cypress/cypress.json --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json", + "cypress:run:reporter": "yarn cypress run --config-file ./cypress.config.ts --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json", "junit:merge": "../../../node_modules/.bin/mochawesome-merge ../../../target/kibana-fleet/cypress/results/mochawesome*.json > ../../../target/kibana-fleet/cypress/results/output.json && ../../../node_modules/.bin/marge ../../../target/kibana-fleet/cypress/results/output.json --reportDir ../../../target/kibana-fleet/cypress/results && mkdir -p ../../../target/junit && cp ../../../target/kibana-fleet/cypress/results/*.xml ../../../target/junit/" } } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.tsx index b34ce952977619..1d0cda70edea4e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.tsx @@ -75,6 +75,7 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{ packagePolicy: NewPackagePolicy; packageInputStreams: Array<RegistryStream & { data_stream: { dataset: string; type: string } }>; packagePolicyInput: NewPackagePolicyInput; + updatePackagePolicy: (updatedPackagePolicy: Partial<NewPackagePolicy>) => void; updatePackagePolicyInput: (updatedInput: Partial<NewPackagePolicyInput>) => void; inputValidationResults: PackagePolicyInputValidationResults; forceShowErrors?: boolean; @@ -85,6 +86,7 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{ packageInputStreams, packagePolicyInput, packagePolicy, + updatePackagePolicy, updatePackagePolicyInput, inputValidationResults, forceShowErrors, @@ -236,6 +238,7 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{ packagePolicy={packagePolicy} packageInputStream={packageInputStream} packagePolicyInputStream={packagePolicyInputStream!} + updatePackagePolicy={updatePackagePolicy} updatePackagePolicyInputStream={( updatedStream: Partial<PackagePolicyInputStream> ) => { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.tsx index 1662f6d0c72140..e1993082b37bf7 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.tsx @@ -17,9 +17,12 @@ import { EuiText, EuiSpacer, EuiButtonEmpty, + EuiTitle, } from '@elastic/eui'; import { useRouteMatch } from 'react-router-dom'; +import { getRegistryDataStreamAssetBaseName } from '../../../../../../../../../common/services'; + import type { NewPackagePolicy, NewPackagePolicyInputStream, @@ -27,6 +30,7 @@ import type { RegistryStream, RegistryVarsEntry, } from '../../../../../../types'; +import { InlineReleaseBadge } from '../../../../../../components'; import type { PackagePolicyConfigValidationResults } from '../../../services'; import { isAdvancedVar, validationHasErrors } from '../../../services'; import { PackagePolicyEditorDatastreamPipelines } from '../../datastream_pipelines'; @@ -48,6 +52,7 @@ export const PackagePolicyInputStreamConfig: React.FunctionComponent<{ packageInputStream: RegistryStream & { data_stream: { dataset: string; type: string } }; packageInfo: PackageInfo; packagePolicyInputStream: NewPackagePolicyInputStream; + updatePackagePolicy: (updatedPackagePolicy: Partial<NewPackagePolicy>) => void; updatePackagePolicyInputStream: (updatedStream: Partial<NewPackagePolicyInputStream>) => void; inputStreamValidationResults: PackagePolicyConfigValidationResults; forceShowErrors?: boolean; @@ -57,6 +62,7 @@ export const PackagePolicyInputStreamConfig: React.FunctionComponent<{ packageInputStream, packageInfo, packagePolicyInputStream, + updatePackagePolicy, updatePackagePolicyInputStream, inputStreamValidationResults, forceShowErrors, @@ -111,150 +117,239 @@ export const PackagePolicyInputStreamConfig: React.FunctionComponent<{ ); return ( - <EuiFlexGrid columns={2} id={isDefaultDatstream ? 'test123' : 'asas'}> - <ScrollAnchor ref={containerRef} /> - <EuiFlexItem> - <EuiFlexGroup gutterSize="none" alignItems="flexStart"> - <EuiFlexItem grow={1} /> - <EuiFlexItem grow={5}> - <EuiSwitch - label={packageInputStream.title} - disabled={packagePolicyInputStream.keep_enabled} - checked={packagePolicyInputStream.enabled} - onChange={(e) => { - const enabled = e.target.checked; - updatePackagePolicyInputStream({ - enabled, - }); - }} - /> - {packageInputStream.description ? ( - <Fragment> - <EuiSpacer size="s" /> - <EuiText size="s" color="subdued"> - <ReactMarkdown>{packageInputStream.description}</ReactMarkdown> - </EuiText> - </Fragment> - ) : null} - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - <FlexItemWithMaxWidth> - <EuiFlexGroup direction="column" gutterSize="m"> - {requiredVars.map((varDef) => { - if (!packagePolicyInputStream?.vars) return null; - const { name: varName, type: varType } = varDef; - const varConfigEntry = packagePolicyInputStream.vars?.[varName]; - const value = varConfigEntry?.value; - const frozen = varConfigEntry?.frozen ?? false; - - return ( - <EuiFlexItem key={varName}> - <PackagePolicyInputVarField - varDef={varDef} - value={value} - frozen={frozen} - onChange={(newValue: any) => { - updatePackagePolicyInputStream({ - vars: { - ...packagePolicyInputStream.vars, - [varName]: { - type: varType, - value: newValue, - }, - }, - }); - }} - errors={inputStreamValidationResults?.vars![varName]} - forceShowErrors={forceShowErrors} - /> - </EuiFlexItem> - ); - })} - {/* Advanced section */} - {(isPackagePolicyEdit || !!advancedVars.length) && ( - <Fragment> - <EuiFlexItem> - <EuiFlexGroup justifyContent="spaceBetween" alignItems="center"> + <> + <EuiFlexGrid columns={2} id={isDefaultDatstream ? 'test123' : 'asas'}> + <ScrollAnchor ref={containerRef} /> + <EuiFlexItem> + <EuiFlexGroup gutterSize="none" alignItems="flexStart"> + <EuiFlexItem grow={1} /> + <EuiFlexItem grow={5}> + <EuiFlexGroup + gutterSize="none" + alignItems="flexStart" + justifyContent="spaceBetween" + > + <EuiFlexItem grow={false}> + <EuiSwitch + label={packageInputStream.title} + disabled={packagePolicyInputStream.keep_enabled} + checked={packagePolicyInputStream.enabled} + onChange={(e) => { + const enabled = e.target.checked; + updatePackagePolicyInputStream({ + enabled, + }); + }} + /> + </EuiFlexItem> + {packagePolicyInputStream.release && packagePolicyInputStream.release !== 'ga' ? ( <EuiFlexItem grow={false}> - <EuiButtonEmpty - size="xs" - iconType={isShowingAdvanced ? 'arrowDown' : 'arrowRight'} - onClick={() => setIsShowingAdvanced(!isShowingAdvanced)} - flush="left" - > - <FormattedMessage - id="xpack.fleet.createPackagePolicy.stepConfigure.toggleAdvancedOptionsButtonText" - defaultMessage="Advanced options" - /> - </EuiButtonEmpty> + <InlineReleaseBadge release={packagePolicyInputStream.release} /> </EuiFlexItem> - {!isShowingAdvanced && hasErrors && advancedVarsWithErrorsCount ? ( + ) : null} + </EuiFlexGroup> + {packageInputStream.description ? ( + <Fragment> + <EuiSpacer size="s" /> + <EuiText size="s" color="subdued"> + <ReactMarkdown>{packageInputStream.description}</ReactMarkdown> + </EuiText> + </Fragment> + ) : null} + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <FlexItemWithMaxWidth> + <EuiFlexGroup direction="column" gutterSize="m"> + {requiredVars.map((varDef) => { + if (!packagePolicyInputStream?.vars) return null; + const { name: varName, type: varType } = varDef; + const varConfigEntry = packagePolicyInputStream.vars?.[varName]; + const value = varConfigEntry?.value; + const frozen = varConfigEntry?.frozen ?? false; + + return ( + <EuiFlexItem key={varName}> + <PackagePolicyInputVarField + varDef={varDef} + value={value} + frozen={frozen} + onChange={(newValue: any) => { + updatePackagePolicyInputStream({ + vars: { + ...packagePolicyInputStream.vars, + [varName]: { + type: varType, + value: newValue, + }, + }, + }); + }} + errors={inputStreamValidationResults?.vars![varName]} + forceShowErrors={forceShowErrors} + /> + </EuiFlexItem> + ); + })} + {/* Advanced section */} + {(isPackagePolicyEdit || !!advancedVars.length) && ( + <Fragment> + <EuiFlexItem> + <EuiFlexGroup justifyContent="spaceBetween" alignItems="center"> <EuiFlexItem grow={false}> - <EuiText color="danger" size="s"> + <EuiButtonEmpty + size="xs" + iconType={isShowingAdvanced ? 'arrowDown' : 'arrowRight'} + onClick={() => setIsShowingAdvanced(!isShowingAdvanced)} + flush="left" + > <FormattedMessage - id="xpack.fleet.createPackagePolicy.stepConfigure.errorCountText" - defaultMessage="{count, plural, one {# error} other {# errors}}" - values={{ count: advancedVarsWithErrorsCount }} + id="xpack.fleet.createPackagePolicy.stepConfigure.toggleAdvancedOptionsButtonText" + defaultMessage="Advanced options" /> - </EuiText> + </EuiButtonEmpty> </EuiFlexItem> - ) : null} - </EuiFlexGroup> - </EuiFlexItem> - {isShowingAdvanced ? ( - <> - {advancedVars.map((varDef) => { - if (!packagePolicyInputStream.vars) return null; - const { name: varName, type: varType } = varDef; - const value = packagePolicyInputStream.vars?.[varName]?.value; + {!isShowingAdvanced && hasErrors && advancedVarsWithErrorsCount ? ( + <EuiFlexItem grow={false}> + <EuiText color="danger" size="s"> + <FormattedMessage + id="xpack.fleet.createPackagePolicy.stepConfigure.errorCountText" + defaultMessage="{count, plural, one {# error} other {# errors}}" + values={{ count: advancedVarsWithErrorsCount }} + /> + </EuiText> + </EuiFlexItem> + ) : null} + </EuiFlexGroup> + </EuiFlexItem> + {isShowingAdvanced ? ( + <> + {advancedVars.map((varDef) => { + if (!packagePolicyInputStream.vars) return null; + const { name: varName, type: varType } = varDef; + const value = packagePolicyInputStream.vars?.[varName]?.value; - return ( - <EuiFlexItem key={varName}> - <PackagePolicyInputVarField - varDef={varDef} - value={value} - onChange={(newValue: any) => { - updatePackagePolicyInputStream({ - vars: { - ...packagePolicyInputStream.vars, - [varName]: { - type: varType, - value: newValue, + return ( + <EuiFlexItem key={varName}> + <PackagePolicyInputVarField + varDef={varDef} + value={value} + onChange={(newValue: any) => { + updatePackagePolicyInputStream({ + vars: { + ...packagePolicyInputStream.vars, + [varName]: { + type: varType, + value: newValue, + }, }, - }, - }); - }} - errors={inputStreamValidationResults?.vars![varName]} - forceShowErrors={forceShowErrors} - /> - </EuiFlexItem> - ); - })} - {/* Only show datastream pipelines and mappings on edit */} - {isPackagePolicyEdit && ( - <> - <EuiFlexItem> - <PackagePolicyEditorDatastreamPipelines - packageInputStream={packagePolicyInputStream} - packageInfo={packageInfo} - /> - </EuiFlexItem> - <EuiFlexItem> - <PackagePolicyEditorDatastreamMappings - packageInputStream={packagePolicyInputStream} - packageInfo={packageInfo} - /> - </EuiFlexItem> - </> - )} - </> - ) : null} - </Fragment> - )} - </EuiFlexGroup> - </FlexItemWithMaxWidth> - </EuiFlexGrid> + }); + }} + errors={inputStreamValidationResults?.vars![varName]} + forceShowErrors={forceShowErrors} + /> + </EuiFlexItem> + ); + })} + {/* Only show datastream pipelines and mappings on edit */} + {isPackagePolicyEdit && ( + <> + <EuiFlexItem> + <PackagePolicyEditorDatastreamPipelines + packageInputStream={packagePolicyInputStream} + packageInfo={packageInfo} + /> + </EuiFlexItem> + <EuiFlexItem> + <PackagePolicyEditorDatastreamMappings + packageInputStream={packagePolicyInputStream} + packageInfo={packageInfo} + /> + </EuiFlexItem> + </> + )} + {/* Experimental index/datastream settings e.g. synthetic source */} + <EuiFlexItem> + <EuiFlexGroup direction="column" gutterSize="xs"> + <EuiFlexItem grow={false}> + <EuiTitle size="xxxs"> + <h5> + <FormattedMessage + id="xpack.fleet.packagePolicyEditor.experimentalSettings.title" + defaultMessage="Indexing settings (experimental)" + /> + </h5> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem> + <EuiText color="subdued" size="xs"> + <FormattedMessage + id="xpack.fleet.createPackagePolicy.stepConfigure.experimentalFeaturesDescription" + defaultMessage="Select data streams to configure indexing options. This is an {experimentalFeature} and may have effects on other properties." + values={{ + experimentalFeature: ( + <strong> + <FormattedMessage + id="xpack.fleet.createPackagePolicy.experimentalFeatureText" + defaultMessage="experimental feature" + /> + </strong> + ), + }} + /> + </EuiText> + </EuiFlexItem> + <EuiSpacer size="s" /> + <EuiFlexItem> + <EuiSwitch + checked={ + packagePolicy.package?.experimental_data_stream_features?.some( + ({ data_stream: dataStream, features }) => + dataStream === + getRegistryDataStreamAssetBaseName( + packagePolicyInputStream.data_stream + ) && features.synthetic_source + ) ?? false + } + label={ + <FormattedMessage + id="xpack.fleet.createPackagePolicy.experimentalFeatures.syntheticSourceLabel" + defaultMessage="Synthetic source" + /> + } + onChange={(e) => { + if (!packagePolicy.package) { + return; + } + + updatePackagePolicy({ + package: { + ...packagePolicy.package, + experimental_data_stream_features: [ + { + data_stream: getRegistryDataStreamAssetBaseName( + packagePolicyInputStream.data_stream + ), + features: { + synthetic_source: e.target.checked, + }, + }, + ], + }, + }); + }} + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </> + ) : null} + </Fragment> + )} + </EuiFlexGroup> + </FlexItemWithMaxWidth> + </EuiFlexGrid> + </> ); } ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_configure_package.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_configure_package.tsx index 57b5376c9fbb7e..541f54b792c7bb 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_configure_package.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_configure_package.tsx @@ -76,6 +76,7 @@ export const StepConfigurePackagePolicy: React.FunctionComponent<{ packagePolicy={packagePolicy} packageInputStreams={packageInputStreams} packagePolicyInput={packagePolicyInput} + updatePackagePolicy={updatePackagePolicy} updatePackagePolicyInput={(updatedInput: Partial<NewPackagePolicyInput>) => { const indexOfUpdatedInput = packagePolicy.inputs.findIndex( (input) => diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx index 88061897abf767..c359bc9679e44a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx @@ -239,6 +239,7 @@ describe('when on the package policy create page', () => { dataset: 'nginx.access', type: 'logs', }, + release: 'experimental', enabled: true, vars: { paths: { @@ -536,6 +537,7 @@ describe('when on the package policy create page', () => { streams: [ { ...newPackagePolicy.inputs[0].streams[0], + release: 'experimental', vars: { paths: { type: 'text', diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx index 2e238d4142f1de..dbe58b252a707e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx @@ -20,11 +20,13 @@ import { EuiBadge, useEuiTheme, } from '@elastic/eui'; +import { filter } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import styled from 'styled-components'; import type { Agent, AgentPolicy, PackagePolicy } from '../../../../../types'; +import type { FleetServerAgentComponentUnit } from '../../../../../../../../common/types/models/agent'; import { useLink, useUIExtension } from '../../../../../hooks'; import { ExtensionWrapper, PackageIcon } from '../../../../../components'; @@ -94,11 +96,16 @@ export const AgentDetailsIntegration: React.FunctionComponent<{ const { getHref } = useLink(); const theme = useEuiTheme(); - const [showNeedsAttentionBadge, setShowNeedsAttentionBadge] = useState(false); + const [isAttentionBadgeNeededForPolicyResponse, setIsAttentionBadgeNeededForPolicyResponse] = + useState(false); const extensionView = useUIExtension( packagePolicy.package?.name ?? '', 'package-policy-response' ); + const genericErrorsListExtensionView = useUIExtension( + packagePolicy.package?.name ?? '', + 'package-generic-errors-list' + ); const policyResponseExtensionView = useMemo(() => { return ( @@ -106,13 +113,41 @@ export const AgentDetailsIntegration: React.FunctionComponent<{ <ExtensionWrapper> <extensionView.Component agent={agent} - onShowNeedsAttentionBadge={setShowNeedsAttentionBadge} + onShowNeedsAttentionBadge={setIsAttentionBadgeNeededForPolicyResponse} /> </ExtensionWrapper> ) ); }, [agent, extensionView]); + const packageErrors = useMemo(() => { + const packageErrorUnits: FleetServerAgentComponentUnit[] = []; + if (!agent.components) { + return packageErrorUnits; + } + + const filteredPackageComponents = filter(agent.components, { + type: packagePolicy.package?.name, + }); + + filteredPackageComponents.forEach((component) => { + packageErrorUnits.push(...filter(component.units, { status: 'failed' })); + }); + return packageErrorUnits; + }, [agent.components, packagePolicy]); + + const showNeedsAttentionBadge = isAttentionBadgeNeededForPolicyResponse || packageErrors.length; + + const genericErrorsListExtensionViewWrapper = useMemo(() => { + return ( + genericErrorsListExtensionView && ( + <ExtensionWrapper> + <genericErrorsListExtensionView.Component packageErrors={packageErrors} /> + </ExtensionWrapper> + ) + ); + }, [packageErrors, genericErrorsListExtensionView]); + const inputItems = [ { label: ( @@ -217,6 +252,7 @@ export const AgentDetailsIntegration: React.FunctionComponent<{ aria-labelledby="inputsTreeView" /> {policyResponseExtensionView} + {genericErrorsListExtensionViewWrapper} <EuiSpacer /> </CollapsablePanel> ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_modal/index.tsx index aee476723f1a95..cae8b00fb6d3d1 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_modal/index.tsx @@ -82,13 +82,20 @@ export const AgentReassignAgentPolicyModal: React.FunctionComponent<Props> = ({ throw res.error; } setIsSubmitting(false); + const hasCompleted = isSingleAgent || Object.keys(res.data ?? {}).length > 0; const successMessage = i18n.translate( 'xpack.fleet.agentReassignPolicy.successSingleNotificationTitle', { defaultMessage: 'Agent policy reassigned', } ); - notifications.toasts.addSuccess(successMessage); + const submittedMessage = i18n.translate( + 'xpack.fleet.agentReassignPolicy.submittedNotificationTitle', + { + defaultMessage: 'Agent policy reassign submitted', + } + ); + notifications.toasts.addSuccess(hasCompleted ? successMessage : submittedMessage); onClose(); } catch (error) { setIsSubmitting(false); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx index 72b1e00c1ed02a..7c0c0136c3d099 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx @@ -40,7 +40,7 @@ export const AgentUnenrollAgentModal: React.FunctionComponent<Props> = ({ async function onSubmit() { try { setIsSubmitting(true); - const { error } = isSingleAgent + const { error, data } = isSingleAgent ? await sendPostAgentUnenroll((agents[0] as Agent).id, { revoke: forceUnenroll, }) @@ -52,6 +52,13 @@ export const AgentUnenrollAgentModal: React.FunctionComponent<Props> = ({ throw error; } setIsSubmitting(false); + const hasCompleted = isSingleAgent || Object.keys(data ?? {}).length > 0; + const submittedMessage = i18n.translate( + 'xpack.fleet.unenrollAgents.submittedNotificationTitle', + { + defaultMessage: 'Agent(s) unenroll submitted', + } + ); if (forceUnenroll) { const successMessage = isSingleAgent ? i18n.translate('xpack.fleet.unenrollAgents.successForceSingleNotificationTitle', { @@ -60,7 +67,7 @@ export const AgentUnenrollAgentModal: React.FunctionComponent<Props> = ({ : i18n.translate('xpack.fleet.unenrollAgents.successForceMultiNotificationTitle', { defaultMessage: 'Agents unenrolled', }); - notifications.toasts.addSuccess(successMessage); + notifications.toasts.addSuccess(hasCompleted ? successMessage : submittedMessage); } else { const successMessage = isSingleAgent ? i18n.translate('xpack.fleet.unenrollAgents.successSingleNotificationTitle', { @@ -69,7 +76,7 @@ export const AgentUnenrollAgentModal: React.FunctionComponent<Props> = ({ : i18n.translate('xpack.fleet.unenrollAgents.successMultiNotificationTitle', { defaultMessage: 'Unenrolling agents', }); - notifications.toasts.addSuccess(successMessage); + notifications.toasts.addSuccess(hasCompleted ? successMessage : submittedMessage); } onClose(); } catch (error) { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx index 07b284058fc7de..c60536961e01fe 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx @@ -192,7 +192,17 @@ export const AgentUpgradeAgentModal: React.FunctionComponent<AgentUpgradeAgentMo ); setIsSubmitting(false); - if (isSingleAgent && counts.success === counts.total) { + const hasCompleted = isSingleAgent || Object.keys(data ?? {}).length > 0; + const submittedMessage = i18n.translate( + 'xpack.fleet.upgradeAgents.submittedNotificationTitle', + { + defaultMessage: 'Agent(s) upgrade submitted', + } + ); + + if (!hasCompleted) { + notifications.toasts.addSuccess(submittedMessage); + } else if (isSingleAgent && counts.success === counts.total) { notifications.toasts.addSuccess( i18n.translate('xpack.fleet.upgradeAgents.successSingleNotificationTitle', { defaultMessage: 'Upgrading {count} agent', diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx index fa96da7ea6acec..a37fdcf5533c86 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx @@ -16,11 +16,10 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { CardIcon } from '../../../../../components/package_icon'; import type { IntegrationCardItem } from '../../../../../../common/types/models/epm'; +import { InlineReleaseBadge } from '../../../components'; import { useStartServices } from '../../../hooks'; import { INTEGRATIONS_BASE_PATH, INTEGRATIONS_PLUGIN_ID } from '../../../constants'; -import { CardReleaseBadge } from './release_badge'; - export type PackageCardProps = IntegrationCardItem; // Min-height is roughly 3 lines of content. @@ -50,7 +49,7 @@ export function PackageCard({ <EuiFlexItem grow={false}> <EuiSpacer size="xs" /> <span> - <CardReleaseBadge release={release} /> + <InlineReleaseBadge release={release} /> </span> </EuiFlexItem> ); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx index 98efc91cfc34c6..e360f615ad3698 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx @@ -15,9 +15,11 @@ import { EuiLink, EuiSpacer, EuiTitle, - EuiSearchBar, + EuiFieldSearch, EuiText, - EuiBadge, + useEuiTheme, + EuiIcon, + EuiScreenReaderOnly, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -69,6 +71,7 @@ export const PackageListGrid: FunctionComponent<Props> = ({ const menuRef = useRef<HTMLDivElement>(null); const [isSticky, setIsSticky] = useState(false); const [windowScrollY] = useState(window.scrollY); + const { euiTheme } = useEuiTheme(); useEffect(() => { const menuRefCurrent = menuRef.current; @@ -81,17 +84,10 @@ export const PackageListGrid: FunctionComponent<Props> = ({ return () => window.removeEventListener('scroll', onScroll); }, [windowScrollY, isSticky]); - const onQueryChange = ({ - queryText, - error, - }: { - queryText: string; - error: { message: string } | null; - }) => { - if (!error) { - onSearchChange(queryText); - setSearchTerm(queryText); - } + const onQueryChange = (e: any) => { + const queryText = e.target.value; + setSearchTerm(queryText); + onSearchChange(queryText); }; const resetQuery = () => { @@ -128,37 +124,64 @@ export const PackageListGrid: FunctionComponent<Props> = ({ <> {featuredList} <div ref={menuRef}> - <EuiFlexGroup alignItems="flexStart" gutterSize="xl"> + <EuiFlexGroup + alignItems="flexStart" + gutterSize="xl" + data-test-subj="epmList.integrationCards" + > <EuiFlexItem grow={1} className={isSticky ? 'kbnStickyMenu' : ''}> {controlsContent} </EuiFlexItem> <EuiFlexItem grow={5}> - <EuiSearchBar - query={searchTerm || undefined} - box={{ - 'data-test-subj': 'epmList.searchBar', - placeholder: i18n.translate('xpack.fleet.epmList.searchPackagesPlaceholder', { - defaultMessage: 'Search for integrations', - }), - incremental: true, - }} - onChange={onQueryChange} - toolsRight={ + <EuiFieldSearch + data-test-subj="epmList.searchBar" + placeholder={i18n.translate('xpack.fleet.epmList.searchPackagesPlaceholder', { + defaultMessage: 'Search for integrations', + })} + value={searchTerm} + onChange={(e) => onQueryChange(e)} + isClearable={true} + incremental={true} + fullWidth={true} + prepend={ selectedCategoryTitle ? ( - <div> - <EuiBadge - color="accent" - iconType="cross" - iconSide="right" - iconOnClick={() => { + <EuiText + data-test-subj="epmList.categoryBadge" + size="xs" + style={{ + display: 'flex', + alignItems: 'center', + fontWeight: euiTheme.font.weight.bold, + backgroundColor: euiTheme.colors.lightestShade, + }} + > + <EuiScreenReaderOnly> + <span>Searching category: </span> + </EuiScreenReaderOnly> + {selectedCategoryTitle} + <button + data-test-subj="epmList.categoryBadge.closeBtn" + onClick={() => { setSelectedCategory(''); }} - iconOnClickAriaLabel="Remove category" - data-test-sub="epmList.categoryBadge" + aria-label="Remove filter" + style={{ + padding: euiTheme.size.xs, + paddingTop: '2px', + }} > - {selectedCategoryTitle} - </EuiBadge> - </div> + <EuiIcon + type="cross" + color="text" + size="s" + style={{ + width: 'auto', + padding: 0, + backgroundColor: euiTheme.colors.lightestShade, + }} + /> + </button> + </EuiText> ) : undefined } /> diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx index d3e97110b57ad0..dfe1c6001cedef 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx @@ -41,12 +41,10 @@ import { useGetPackageInfoByKey, useLink, useAgentPolicyContext } from '../../.. import { pkgKeyFromPackageInfo } from '../../../../services'; import type { DetailViewPanelName, PackageInfo } from '../../../../types'; import { InstallStatus } from '../../../../types'; -import { Error, Loading } from '../../../../components'; +import { Error, Loading, HeaderReleaseBadge } from '../../../../components'; import type { WithHeaderLayoutProps } from '../../../../layouts'; import { WithHeaderLayout } from '../../../../layouts'; -import { HeaderReleaseBadge } from '../../components/release_badge'; - import { useIsFirstTimeAgentUser } from './hooks'; import { getInstallPkgRouteOptions } from './utils'; import { diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/category_facets.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/category_facets.tsx index 573701ae9a6fb1..9f88e2c391b3d6 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/category_facets.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/category_facets.tsx @@ -64,6 +64,7 @@ export function CategoryFacets({ categories.map((category) => { return ( <EuiFacetButton + data-test-subj={`epmList.categories.${category.id}`} isSelected={category.id === selectedCategory} key={category.id} id={category.id} diff --git a/x-pack/plugins/fleet/public/components/index.ts b/x-pack/plugins/fleet/public/components/index.ts index e1a1e608871720..4e9d9f0f021b0a 100644 --- a/x-pack/plugins/fleet/public/components/index.ts +++ b/x-pack/plugins/fleet/public/components/index.ts @@ -25,3 +25,4 @@ export * from './link_and_revision'; export * from './agent_enrollment_flyout'; export * from './platform_selector'; export { ConfirmForceInstallModal } from './confirm_force_install_modal'; +export { HeaderReleaseBadge, InlineReleaseBadge } from './release_badge'; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/release_badge.tsx b/x-pack/plugins/fleet/public/components/release_badge.tsx similarity index 92% rename from x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/release_badge.tsx rename to x-pack/plugins/fleet/public/components/release_badge.tsx index 3032b61d563337..3252ea21ee2dff 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/release_badge.tsx +++ b/x-pack/plugins/fleet/public/components/release_badge.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import type { PackageInfo, RegistryRelease } from '../../../types'; +import type { PackageInfo, RegistryRelease } from '../types'; const RELEASE_BADGE_LABEL: { [key in Exclude<RegistryRelease, 'ga'>]: string } = { beta: i18n.translate('xpack.fleet.epm.releaseBadge.betaLabel', { @@ -43,7 +43,7 @@ export const HeaderReleaseBadge: React.FC<{ release: NonNullable<PackageInfo['re ); }; -export const CardReleaseBadge: React.FC<{ release: NonNullable<PackageInfo['release']> }> = ({ +export const InlineReleaseBadge: React.FC<{ release: NonNullable<PackageInfo['release']> }> = ({ release, }) => { if (release === 'ga') return null; diff --git a/x-pack/plugins/fleet/public/hooks/use_request/agents.ts b/x-pack/plugins/fleet/public/hooks/use_request/agents.ts index da082a1ff0d459..13f687e321e548 100644 --- a/x-pack/plugins/fleet/public/hooks/use_request/agents.ts +++ b/x-pack/plugins/fleet/public/hooks/use_request/agents.ts @@ -6,6 +6,7 @@ */ import type { + GetActionStatusResponse, GetAgentTagsResponse, PostBulkUpdateAgentTagsRequest, UpdateAgentRequest, @@ -195,6 +196,13 @@ export function sendPostBulkAgentUpgrade( }); } +export function sendGetActionStatus() { + return sendRequest<GetActionStatusResponse>({ + path: agentRouteService.getActionStatusPath(), + method: 'get', + }); +} + export function sendGetCurrentUpgrades() { return sendRequest<GetCurrentUpgradesResponse>({ path: agentRouteService.getCurrentUpgradesPath(), diff --git a/x-pack/plugins/fleet/public/index.ts b/x-pack/plugins/fleet/public/index.ts index d510d57c522573..a55937999c4d66 100644 --- a/x-pack/plugins/fleet/public/index.ts +++ b/x-pack/plugins/fleet/public/index.ts @@ -42,6 +42,8 @@ export type { PackagePolicyResponseExtension, PackagePolicyResponseExtensionComponent, PackagePolicyResponseExtensionComponentProps, + PackageGenericErrorsListProps, + PackageGenericErrorsListComponent, UIExtensionPoint, UIExtensionRegistrationCallback, UIExtensionsStorage, diff --git a/x-pack/plugins/fleet/public/types/index.ts b/x-pack/plugins/fleet/public/types/index.ts index 72755e74d858f7..2438272503ac78 100644 --- a/x-pack/plugins/fleet/public/types/index.ts +++ b/x-pack/plugins/fleet/public/types/index.ts @@ -26,6 +26,7 @@ export type { DownloadSource, DataStream, Settings, + ActionStatus, CurrentUpgrade, GetFleetStatusResponse, GetAgentPoliciesRequest, diff --git a/x-pack/plugins/fleet/public/types/ui_extensions.ts b/x-pack/plugins/fleet/public/types/ui_extensions.ts index 17e9a25c656d05..c3a9f05fc7d9fc 100644 --- a/x-pack/plugins/fleet/public/types/ui_extensions.ts +++ b/x-pack/plugins/fleet/public/types/ui_extensions.ts @@ -8,6 +8,8 @@ import type { EuiStepProps } from '@elastic/eui'; import type { ComponentType, LazyExoticComponent } from 'react'; +import type { FleetServerAgentComponentUnit } from '../../common/types/models/agent'; + import type { Agent, NewPackagePolicy, PackageInfo, PackagePolicy } from '.'; /** Register a Fleet UI extension */ @@ -60,6 +62,17 @@ export interface PackagePolicyResponseExtensionComponentProps { onShowNeedsAttentionBadge?: (val: boolean) => void; } +/** + * UI Component Extension is used on the pages displaying the ability to see + * a generic endpoint errors list + */ +export type PackageGenericErrorsListComponent = ComponentType<PackageGenericErrorsListProps>; + +export interface PackageGenericErrorsListProps { + /** A list of errors from a package */ + packageErrors: FleetServerAgentComponentUnit[]; +} + /** Extension point registration contract for Integration Policy Edit views */ export interface PackagePolicyEditExtension { package: string; @@ -74,6 +87,12 @@ export interface PackagePolicyResponseExtension { Component: LazyExoticComponent<PackagePolicyResponseExtensionComponent>; } +export interface PackageGenericErrorsListExtension { + package: string; + view: 'package-generic-errors-list'; + Component: LazyExoticComponent<PackageGenericErrorsListComponent>; +} + /** Extension point registration contract for Integration Policy Edit tabs views */ export interface PackagePolicyEditTabsExtension { package: string; @@ -158,4 +177,5 @@ export type UIExtensionPoint = | PackageCustomExtension | PackagePolicyCreateExtension | PackageAssetsExtension + | PackageGenericErrorsListExtension | AgentEnrollmentFlyoutFinalStepExtension; diff --git a/x-pack/plugins/fleet/scripts/create_agents/create_agents.ts b/x-pack/plugins/fleet/scripts/create_agents/create_agents.ts new file mode 100644 index 00000000000000..31df24c70a5d4b --- /dev/null +++ b/x-pack/plugins/fleet/scripts/create_agents/create_agents.ts @@ -0,0 +1,143 @@ +/* + * 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 fetch from 'node-fetch'; +import { ToolingLog } from '@kbn/tooling-log'; +import uuid from 'uuid/v4'; + +const KIBANA_URL = 'http://localhost:5601'; +const KIBANA_USERNAME = 'elastic'; +const KIBANA_PASSWORD = 'changeme'; + +const ES_URL = 'http://localhost:9200'; +const ES_SUPERUSER = 'fleet_superuser'; +const ES_PASSWORD = 'password'; + +async function createAgentDocsBulk(policyId: string, count: number) { + const auth = 'Basic ' + Buffer.from(ES_SUPERUSER + ':' + ES_PASSWORD).toString('base64'); + const body = ( + '{ "index":{ } }\n' + + JSON.stringify({ + access_api_key_id: 'api-key-1', + active: true, + policy_id: policyId, + type: 'PERMANENT', + local_metadata: { + elastic: { + agent: { + snapshot: false, + upgradeable: true, + version: '8.2.0', + }, + }, + host: { hostname: uuid() }, + }, + user_provided_metadata: {}, + enrolled_at: new Date().toISOString(), + last_checkin: new Date().toISOString(), + tags: ['script_create_agents'], + }) + + '\n' + ).repeat(count); + const res = await fetch(`${ES_URL}/.fleet-agents/_bulk`, { + method: 'post', + body, + headers: { + Authorization: auth, + 'Content-Type': 'application/x-ndjson', + }, + }); + const data = await res.json(); + return data; +} + +async function createSuperUser() { + const auth = 'Basic ' + Buffer.from(KIBANA_USERNAME + ':' + KIBANA_PASSWORD).toString('base64'); + const roleRes = await fetch(`${ES_URL}/_security/role/${ES_SUPERUSER}`, { + method: 'post', + body: JSON.stringify({ + indices: [ + { + names: ['.fleet*'], + privileges: ['all'], + allow_restricted_indices: true, + }, + ], + }), + headers: { + Authorization: auth, + 'Content-Type': 'application/json', + }, + }); + const role = await roleRes.json(); + const userRes = await fetch(`${ES_URL}/_security/user/${ES_SUPERUSER}`, { + method: 'post', + body: JSON.stringify({ + password: ES_PASSWORD, + roles: ['superuser', ES_SUPERUSER], + }), + headers: { + Authorization: auth, + 'Content-Type': 'application/json', + }, + }); + const user = await userRes.json(); + return { role, user }; +} + +async function createAgentPolicy(id: string) { + const auth = 'Basic ' + Buffer.from(KIBANA_USERNAME + ':' + KIBANA_PASSWORD).toString('base64'); + const res = await fetch(`${KIBANA_URL}/api/fleet/agent_policies`, { + method: 'post', + body: JSON.stringify({ + id, + name: id, + namespace: 'default', + description: '', + monitoring_enabled: ['logs'], + data_output_id: 'fleet-default-output', + monitoring_output_id: 'fleet-default-output', + }), + headers: { + Authorization: auth, + 'Content-Type': 'application/json', + 'kbn-xsrf': 'kibana', + 'x-elastic-product-origin': 'fleet', + }, + }); + const data = await res.json(); + return data; +} + +/** + * Script to create large number of agent documents at once. + * This is helpful for testing agent bulk actions locally as the kibana async logic kicks in for >10k agents. + */ +export async function run() { + const logger = new ToolingLog({ + level: 'info', + writeTo: process.stdout, + }); + + logger.info('Creating agent policy'); + + const agentPolicyId = uuid(); + const agentPolicy = await createAgentPolicy(agentPolicyId); + logger.info(`Created agent policy ${agentPolicy.item.id}`); + + logger.info('Creating fleet superuser'); + const { role, user } = await createSuperUser(); + logger.info(`Created role ${ES_SUPERUSER}, created: ${role.role.created}`); + logger.info(`Created user ${ES_SUPERUSER}, created: ${user.created}`); + + logger.info('Creating agent documents'); + const count = 50000; + const agents = await createAgentDocsBulk(agentPolicyId, count); + logger.info( + `Created ${agents.items.length} agent docs, took ${agents.took}, errors: ${agents.errors}` + ); +} diff --git a/x-pack/plugins/fleet/scripts/create_agents/index.js b/x-pack/plugins/fleet/scripts/create_agents/index.js new file mode 100644 index 00000000000000..62614b67ea69bf --- /dev/null +++ b/x-pack/plugins/fleet/scripts/create_agents/index.js @@ -0,0 +1,17 @@ +/* + * 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. + */ + +require('../../../../../src/setup_node_env'); +require('./create_agents').run(); + +/* +Usage: + +cd x-pack/plugins/fleet +node scripts/create_agents/index.js + +*/ diff --git a/x-pack/plugins/fleet/server/errors/handlers.ts b/x-pack/plugins/fleet/server/errors/handlers.ts index da6d245f456a6a..173d295c079c29 100644 --- a/x-pack/plugins/fleet/server/errors/handlers.ts +++ b/x-pack/plugins/fleet/server/errors/handlers.ts @@ -29,6 +29,7 @@ import { RegistryError, RegistryResponseError, PackageFailedVerificationError, + PackagePolicyNotFoundError, } from '.'; type IngestErrorHandler = ( @@ -52,7 +53,7 @@ const getHTTPResponseCode = (error: IngestManagerError): number => { // Connection errors (ie. RegistryConnectionError) / fallback (RegistryError) from EPR return 502; // Bad Gateway } - if (error instanceof PackageNotFoundError) { + if (error instanceof PackageNotFoundError || error instanceof PackagePolicyNotFoundError) { return 404; // Not Found } if (error instanceof AgentPolicyNameExistsError) { diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index a76988506cad21..f3f9e5b59d3c49 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -73,6 +73,7 @@ export const createAppContextStartContractMock = ( kibanaVersion: '8.99.0', // Fake version :) kibanaBranch: 'main', telemetryEventsSender: createMockTelemetryEventsSender(), + bulkActionsResolver: {} as any, }; }; diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 0cec7c92c62b1e..57e65664d59690 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -37,6 +37,10 @@ import type { } from '@kbn/encrypted-saved-objects-plugin/server'; import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; import type { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server'; +import type { + TaskManagerSetupContract, + TaskManagerStartContract, +} from '@kbn/task-manager-plugin/server'; import type { CloudSetup } from '@kbn/cloud-plugin/server'; @@ -100,6 +104,7 @@ import { FleetArtifactsClient } from './services/artifacts'; import type { FleetRouter } from './types/request_context'; import { TelemetryEventsSender } from './telemetry/sender'; import { setupFleet } from './services/setup'; +import { BulkActionsResolver } from './services/agents'; export interface FleetSetupDeps { security: SecurityPluginSetup; @@ -109,6 +114,7 @@ export interface FleetSetupDeps { usageCollection?: UsageCollectionSetup; spaces: SpacesPluginStart; telemetry?: TelemetryPluginSetup; + taskManager: TaskManagerSetupContract; } export interface FleetStartDeps { @@ -118,6 +124,7 @@ export interface FleetStartDeps { security: SecurityPluginStart; telemetry?: TelemetryPluginStart; savedObjectsTagging: SavedObjectTaggingStart; + taskManager: TaskManagerStartContract; } export interface FleetAppContext { @@ -139,6 +146,7 @@ export interface FleetAppContext { logger?: Logger; httpSetup?: HttpServiceSetup; telemetryEventsSender: TelemetryEventsSender; + bulkActionsResolver: BulkActionsResolver; } export type FleetSetupContract = void; @@ -203,6 +211,7 @@ export class FleetPlugin private encryptedSavedObjectsSetup?: EncryptedSavedObjectsPluginSetup; private readonly telemetryEventsSender: TelemetryEventsSender; private readonly fleetStatus$: BehaviorSubject<ServiceStatus>; + private bulkActionsResolver?: BulkActionsResolver; private agentService?: AgentService; private packageService?: PackageService; @@ -388,6 +397,7 @@ export class FleetPlugin } this.telemetryEventsSender.setup(deps.telemetry); + this.bulkActionsResolver = new BulkActionsResolver(deps.taskManager, core); } public start(core: CoreStart, plugins: FleetStartDeps): FleetStartContract { @@ -412,10 +422,12 @@ export class FleetPlugin cloud: this.cloud, logger: this.logger, telemetryEventsSender: this.telemetryEventsSender, + bulkActionsResolver: this.bulkActionsResolver!, }); licenseService.start(plugins.licensing.license$); this.telemetryEventsSender.start(plugins.telemetry, core); + this.bulkActionsResolver?.start(plugins.taskManager); const logger = appContextService.getLogger(); diff --git a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts index 61d86c983fc6ef..e64af66460ef46 100644 --- a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts @@ -88,6 +88,7 @@ describe('test actions handlers', () => { }), createAgentAction: jest.fn().mockReturnValueOnce(agentAction), cancelAgentAction: jest.fn(), + getAgentActions: jest.fn(), } as jest.Mocked<ActionsService>; const postNewAgentActionHandler = postNewAgentActionHandlerBuilder(actionsService); diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index d6277cd4983fcb..b7d1a4907d8e0a 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -29,6 +29,7 @@ import type { PostBulkUpdateAgentTagsResponse, GetAgentTagsResponse, GetAvailableVersionsResponse, + GetActionStatusResponse, } from '../../../common/types'; import type { GetAgentsRequestSchema, @@ -149,7 +150,7 @@ export const bulkUpdateAgentTagsHandler: RequestHandler< request.body.tagsToRemove ?? [] ); - const body = results.items.reduce<PostBulkUpdateAgentTagsResponse>((acc, so) => { + const body = results.items.reduce<PostBulkUpdateAgentTagsResponse>((acc: any, so: any) => { acc[so.id] = { success: !so.error, error: so.error?.message, @@ -157,7 +158,7 @@ export const bulkUpdateAgentTagsHandler: RequestHandler< return acc; }, {}); - return response.ok({ body }); + return response.ok({ body: { ...body, actionId: results.actionId } }); } catch (error) { return defaultIngestErrorHandler({ error, response }); } @@ -273,7 +274,7 @@ export const postBulkAgentsReassignHandler: RequestHandler< return acc; }, {}); - return response.ok({ body }); + return response.ok({ body: { ...body, actionId: results.actionId } }); } catch (error) { return defaultIngestErrorHandler({ error, response }); } @@ -362,3 +363,16 @@ export const getAvailableVersionsHandler: RequestHandler = async (context, reque return defaultIngestErrorHandler({ error, response }); } }; + +export const getActionStatusHandler: RequestHandler = async (context, request, response) => { + const coreContext = await context.core; + const esClient = coreContext.elasticsearch.client.asInternalUser; + + try { + const actionStatuses = await AgentService.getActionStatuses(esClient); + const body: GetActionStatusResponse = { items: actionStatuses }; + return response.ok({ body }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } +}; diff --git a/x-pack/plugins/fleet/server/routes/agent/index.ts b/x-pack/plugins/fleet/server/routes/agent/index.ts index 7988a9883114e7..b551cd8d39bf4f 100644 --- a/x-pack/plugins/fleet/server/routes/agent/index.ts +++ b/x-pack/plugins/fleet/server/routes/agent/index.ts @@ -41,6 +41,7 @@ import { getAgentDataHandler, bulkUpdateAgentTagsHandler, getAvailableVersionsHandler, + getActionStatusHandler, } from './handlers'; import { postNewAgentActionHandlerBuilder, @@ -134,6 +135,7 @@ export const registerAPIRoutes = (router: FleetAuthzRouter, config: FleetConfigT getAgent: AgentService.getAgentById, cancelAgentAction: AgentService.cancelAgentAction, createAgentAction: AgentService.createAgentAction, + getAgentActions: AgentService.getAgentActions, }) ); @@ -149,6 +151,7 @@ export const registerAPIRoutes = (router: FleetAuthzRouter, config: FleetConfigT getAgent: AgentService.getAgentById, cancelAgentAction: AgentService.cancelAgentAction, createAgentAction: AgentService.createAgentAction, + getAgentActions: AgentService.getAgentActions, }) ); @@ -241,6 +244,18 @@ export const registerAPIRoutes = (router: FleetAuthzRouter, config: FleetConfigT getCurrentUpgradesHandler ); + // Current actions + router.get( + { + path: AGENT_API_ROUTES.ACTION_STATUS_PATTERN, + validate: false, + fleetAuthz: { + fleet: { all: true }, + }, + }, + getActionStatusHandler + ); + // Bulk reassign router.post( { diff --git a/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts b/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts index b6e398d269a6f8..8cc4a7b13e6878 100644 --- a/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts @@ -67,7 +67,7 @@ export const postBulkAgentsUnenrollHandler: RequestHandler< return acc; }, {}); - return response.ok({ body }); + return response.ok({ body: { ...body, actionId: results.actionId } }); } catch (error) { return defaultIngestErrorHandler({ error, response }); } diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts index d75e3ad07d9b87..ff78da6e63d1a2 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts @@ -133,7 +133,7 @@ export const postBulkAgentsUpgradeHandler: RequestHandler< return acc; }, {}); - return response.ok({ body }); + return response.ok({ body: { ...body, actionId: results.actionId } }); } catch (error) { return defaultIngestErrorHandler({ error, response }); } diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index bd031cf9b02741..55d38b00dec3de 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -271,6 +271,18 @@ const getSavedObjectTypes = ( install_status: { type: 'keyword' }, install_source: { type: 'keyword' }, install_format_schema_version: { type: 'version' }, + experimental_data_stream_features: { + type: 'nested', + properties: { + data_stream: { type: 'keyword' }, + features: { + type: 'nested', + properties: { + synthetic_source: { type: 'boolean' }, + }, + }, + }, + }, }, }, migrations: { diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index ffd71c2d6af069..606cf82f10b31f 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -469,8 +469,10 @@ class AgentPolicyService { await packagePolicyService.bulkCreate( soClient, esClient, - newPackagePolicies, - newAgentPolicy.id, + newPackagePolicies.map((newPackagePolicy) => ({ + ...newPackagePolicy, + policy_id: newAgentPolicy.id, + })), { ...options, bumpRevision: false, diff --git a/x-pack/plugins/fleet/server/services/agents/action_runner.ts b/x-pack/plugins/fleet/server/services/agents/action_runner.ts new file mode 100644 index 00000000000000..634bf27ba23ef6 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/action_runner.ts @@ -0,0 +1,188 @@ +/* + * 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 uuid from 'uuid'; +import type { SortResults } from '@elastic/elasticsearch/lib/api/types'; +import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; +import { withSpan } from '@kbn/apm-utils'; + +import { isResponseError } from '@kbn/es-errors'; + +import type { Agent, BulkActionResult } from '../../types'; +import { appContextService } from '..'; +import { SO_SEARCH_LIMIT } from '../../../common/constants'; + +import { getAgentActions } from './actions'; +import { closePointInTime, getAgentsByKuery } from './crud'; + +export interface ActionParams { + kuery: string; + showInactive?: boolean; + batchSize?: number; + total?: number; + actionId?: string; + // additional parameters specific to an action e.g. reassign to new policy id + [key: string]: any; +} + +export interface RetryParams { + pitId: string; + searchAfter?: SortResults; + retryCount?: number; + taskId?: string; +} + +export abstract class ActionRunner { + protected esClient: ElasticsearchClient; + protected soClient: SavedObjectsClientContract; + + protected actionParams: ActionParams; + protected retryParams: RetryParams; + + constructor( + esClient: ElasticsearchClient, + soClient: SavedObjectsClientContract, + actionParams: ActionParams, + retryParams: RetryParams + ) { + this.esClient = esClient; + this.soClient = soClient; + this.actionParams = { ...actionParams, actionId: actionParams.actionId ?? uuid() }; + this.retryParams = retryParams; + } + + protected abstract getActionType(): string; + + protected abstract getTaskType(): string; + + protected abstract processAgents(agents: Agent[]): Promise<{ items: BulkActionResult[] }>; + + /** + * Common runner logic accross all agent bulk actions + * Starts action execution immeditalely, asynchronously + * On errors, starts a task with Task Manager to retry max 3 times + * If the last batch was stored in state, retry continues from there (searchAfter) + */ + public async runActionAsyncWithRetry(): Promise<{ items: BulkActionResult[]; actionId: string }> { + appContextService + .getLogger() + .info( + `Running action asynchronously, actionId: ${this.actionParams.actionId}, total agents: ${this.actionParams.total}` + ); + + withSpan({ name: this.getActionType(), type: 'action' }, () => + this.processAgentsInBatches().catch(async (error) => { + // 404 error comes when PIT query is closed + if (isResponseError(error) && error.statusCode === 404) { + const errorMessage = + '404 error from elasticsearch, not retrying. Error: ' + error.message; + appContextService.getLogger().warn(errorMessage); + return; + } + if (this.retryParams.retryCount) { + appContextService + .getLogger() + .error( + `Retry #${this.retryParams.retryCount} of task ${this.retryParams.taskId} failed: ${error.message}` + ); + + if (this.retryParams.retryCount === 3) { + const errorMessage = 'Stopping after 3rd retry. Error: ' + error.message; + appContextService.getLogger().warn(errorMessage); + return; + } + } else { + appContextService.getLogger().error(`Action failed: ${error.message}`); + } + const taskId = await appContextService.getBulkActionsResolver()!.run( + this.actionParams, + { + ...this.retryParams, + retryCount: (this.retryParams.retryCount ?? 0) + 1, + }, + this.getTaskType() + ); + + appContextService.getLogger().info(`Retrying in task: ${taskId}`); + }) + ); + + return { items: [], actionId: this.actionParams.actionId! }; + } + + private async processBatch(agents: Agent[]): Promise<{ items: BulkActionResult[] }> { + if (this.retryParams.retryCount) { + try { + const actions = await getAgentActions(this.esClient, this.actionParams!.actionId!); + + // skipping batch if there is already an action document present with last agent ids + for (const action of actions) { + if (action.agents?.[0] === agents[0].id) { + return { items: [] }; + } + } + } catch (error) { + appContextService.getLogger().debug(error.message); // if action not found, swallow + } + } + + return await this.processAgents(agents); + } + + async processAgentsInBatches(): Promise<{ items: BulkActionResult[] }> { + const start = Date.now(); + const pitId = this.retryParams.pitId; + + const perPage = this.actionParams.batchSize ?? SO_SEARCH_LIMIT; + + const getAgents = () => + getAgentsByKuery(this.esClient, { + kuery: this.actionParams.kuery, + showInactive: this.actionParams.showInactive ?? false, + page: 1, + perPage, + pitId, + searchAfter: this.retryParams.searchAfter, + }); + + const res = await getAgents(); + + let currentAgents = res.agents; + if (currentAgents.length === 0) { + appContextService + .getLogger() + .debug('currentAgents returned 0 hits, returning from bulk action query'); + return { items: [] }; // stop executing if there are no more results + } + + let results = await this.processBatch(currentAgents); + let allAgentsProcessed = currentAgents.length; + + while (allAgentsProcessed < res.total) { + const lastAgent = currentAgents[currentAgents.length - 1]; + this.retryParams.searchAfter = lastAgent.sort!; + const nextPage = await getAgents(); + currentAgents = nextPage.agents; + if (currentAgents.length === 0) { + appContextService + .getLogger() + .debug('currentAgents returned 0 hits, returning from bulk action query'); + break; // stop executing if there are no more results + } + const currentResults = await this.processBatch(currentAgents); + results = { items: results.items.concat(currentResults.items) }; + allAgentsProcessed += currentAgents.length; + } + + await closePointInTime(this.esClient, pitId!); + + appContextService + .getLogger() + .info(`processed ${allAgentsProcessed} agents, took ${Date.now() - start}ms`); + return { ...results }; + } +} diff --git a/x-pack/plugins/fleet/server/services/agents/action_status.ts b/x-pack/plugins/fleet/server/services/agents/action_status.ts new file mode 100644 index 00000000000000..bfda349ac3a05a --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/action_status.ts @@ -0,0 +1,142 @@ +/* + * 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 { ElasticsearchClient } from '@kbn/core/server'; +import pMap from 'p-map'; + +import { SO_SEARCH_LIMIT } from '../../constants'; + +import type { FleetServerAgentAction, ActionStatus } from '../../types'; +import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../common'; + +/** + * Return current bulk actions + */ +export async function getActionStatuses(esClient: ElasticsearchClient): Promise<ActionStatus[]> { + let actions = await _getActions(esClient); + const cancelledActionIds = await _getCancelledActionId(esClient); + + // Fetch acknowledged result for every action + actions = await pMap( + actions, + async (action) => { + const { count } = await esClient.count({ + index: AGENT_ACTIONS_RESULTS_INDEX, + ignore_unavailable: true, + query: { + bool: { + must: [ + { + term: { + action_id: action.actionId, + }, + }, + ], + }, + }, + }); + + const nbAgentsActioned = action.nbAgentsActioned || action.nbAgentsActionCreated; + const complete = count === nbAgentsActioned; + const isCancelled = cancelledActionIds.indexOf(action.actionId) > -1; + + return { + ...action, + nbAgentsAck: count, + status: complete ? 'complete' : isCancelled ? 'cancelled' : action.status, + nbAgentsActioned, + }; + }, + { concurrency: 20 } + ); + + return actions; +} + +async function _getCancelledActionId(esClient: ElasticsearchClient) { + const res = await esClient.search<FleetServerAgentAction>({ + index: AGENT_ACTIONS_INDEX, + ignore_unavailable: true, + size: SO_SEARCH_LIMIT, + query: { + bool: { + must: [ + { + term: { + type: 'CANCEL', + }, + }, + { + exists: { + field: 'agents', + }, + }, + ], + }, + }, + }); + + return res.hits.hits.map((hit) => hit._source?.data?.target_id as string); +} + +async function _getActions(esClient: ElasticsearchClient) { + const res = await esClient.search<FleetServerAgentAction>({ + index: AGENT_ACTIONS_INDEX, + ignore_unavailable: true, + size: SO_SEARCH_LIMIT, + query: { + bool: { + must_not: [ + { + term: { + type: 'CANCEL', + }, + }, + ], + must: [ + { + exists: { + field: 'agents', + }, + }, + ], + }, + }, + body: { + sort: [{ '@timestamp': 'desc' }], + }, + }); + + return Object.values( + res.hits.hits.reduce((acc, hit) => { + if (!hit._source || !hit._source.action_id) { + return acc; + } + + if (!acc[hit._source.action_id]) { + const startTime = hit._source?.start_time ?? hit._source?.['@timestamp']; + const isExpired = hit._source?.expiration + ? Date.parse(hit._source?.expiration) < Date.now() + : false; + acc[hit._source.action_id] = { + actionId: hit._source.action_id, + nbAgentsActionCreated: 0, + nbAgentsAck: 0, + version: hit._source.data?.version as string, + startTime, + type: hit._source?.type, + nbAgentsActioned: hit._source?.total ?? 0, + status: isExpired ? 'expired' : 'in progress', + }; + } + + acc[hit._source.action_id].nbAgentsActionCreated += hit._source.agents?.length ?? 0; + + return acc; + }, {} as { [k: string]: ActionStatus }) + ); +} diff --git a/x-pack/plugins/fleet/server/services/agents/actions.ts b/x-pack/plugins/fleet/server/services/agents/actions.ts index f0e2d059a98a86..a2ba066db7d3fc 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.ts @@ -36,6 +36,7 @@ export async function createAgentAction( type: newAgentAction.type, start_time: newAgentAction.start_time, minimum_execution_duration: newAgentAction.minimum_execution_duration, + total: newAgentAction.total, }; await esClient.create({ @@ -96,6 +97,33 @@ export async function bulkCreateAgentActions( return actions; } +export async function getAgentActions(esClient: ElasticsearchClient, actionId: string) { + const res = await esClient.search<FleetServerAgentAction>({ + index: AGENT_ACTIONS_INDEX, + query: { + bool: { + must: [ + { + term: { + action_id: actionId, + }, + }, + ], + }, + }, + size: SO_SEARCH_LIMIT, + }); + + if (res.hits.hits.length === 0) { + throw new AgentActionNotFoundError('Action not found'); + } + + return res.hits.hits.map((hit) => ({ + ...hit._source, + id: hit._id, + })); +} + export async function cancelAgentAction(esClient: ElasticsearchClient, actionId: string) { const res = await esClient.search<FleetServerAgentAction>({ index: AGENT_ACTIONS_INDEX, @@ -163,4 +191,6 @@ export interface ActionsService { esClient: ElasticsearchClient, newAgentAction: Omit<AgentAction, 'id'> ) => Promise<AgentAction>; + + getAgentActions: (esClient: ElasticsearchClient, actionId: string) => Promise<any[]>; } diff --git a/x-pack/plugins/fleet/server/services/agents/bulk_actions_resolver.ts b/x-pack/plugins/fleet/server/services/agents/bulk_actions_resolver.ts new file mode 100644 index 00000000000000..e80db905d48e0a --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/bulk_actions_resolver.ts @@ -0,0 +1,161 @@ +/* + * 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 { SavedObjectsClient } from '@kbn/core/server'; +import type { CoreSetup, ElasticsearchClient } from '@kbn/core/server'; +import type { + ConcreteTaskInstance, + TaskManagerStartContract, + TaskManagerSetupContract, +} from '@kbn/task-manager-plugin/server'; +import moment from 'moment'; + +import { appContextService } from '../app_context'; + +import { ReassignActionRunner } from './reassign_action_runner'; +import { UpgradeActionRunner } from './upgrade_action_runner'; +import { UpdateAgentTagsActionRunner } from './update_agent_tags_action_runner'; +import { UnenrollActionRunner } from './unenroll_action_runner'; +import type { ActionParams, RetryParams } from './action_runner'; + +export enum BulkActionTaskType { + REASSIGN_RETRY = 'fleet:reassign_action:retry', + UNENROLL_RETRY = 'fleet:unenroll_action:retry', + UPGRADE_RETRY = 'fleet:upgrade_action:retry', + UPDATE_AGENT_TAGS_RETRY = 'fleet:update_agent_tags:retry', +} + +/** + * Create and run retry tasks of agent bulk actions + */ +export class BulkActionsResolver { + private taskManager?: TaskManagerStartContract; + + createTaskRunner(core: CoreSetup, taskType: BulkActionTaskType) { + return ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => { + const getDeps = async () => { + const [coreStart] = await core.getStartServices(); + return { + esClient: coreStart.elasticsearch.client.asInternalUser, + soClient: new SavedObjectsClient(coreStart.savedObjects.createInternalRepository()), + }; + }; + + const runnerMap = { + [BulkActionTaskType.UNENROLL_RETRY]: UnenrollActionRunner, + [BulkActionTaskType.REASSIGN_RETRY]: ReassignActionRunner, + [BulkActionTaskType.UPDATE_AGENT_TAGS_RETRY]: UpdateAgentTagsActionRunner, + [BulkActionTaskType.UPGRADE_RETRY]: UpgradeActionRunner, + }; + + return createRetryTask( + taskInstance, + getDeps, + async ( + esClient: ElasticsearchClient, + soClient: SavedObjectsClient, + actionParams: ActionParams, + retryParams: RetryParams + ) => + await new runnerMap[taskType]( + esClient, + soClient, + actionParams, + retryParams + ).runActionAsyncWithRetry() + ); + }; + } + + constructor(taskManager: TaskManagerSetupContract, core: CoreSetup) { + const definitions = Object.values(BulkActionTaskType) + .map((type) => { + return [ + type, + { + title: 'Bulk Action Retry', + timeout: '1m', + maxAttempts: 1, + createTaskRunner: this.createTaskRunner(core, type), + }, + ]; + }) + .reduce((acc, current) => { + acc[current[0] as string] = current[1]; + return acc; + }, {} as any); + taskManager.registerTaskDefinitions(definitions); + } + + public async start(taskManager: TaskManagerStartContract) { + this.taskManager = taskManager; + } + + getTaskId(actionId: string, type: string) { + return `${type}:${actionId}`; + } + + public async run( + actionParams: ActionParams, + retryParams: RetryParams, + taskType: string, + runAt?: Date + ) { + const taskId = this.getTaskId(actionParams.actionId!, taskType); + await this.taskManager?.ensureScheduled({ + id: taskId, + taskType, + scope: ['fleet'], + state: {}, + params: { actionParams, retryParams }, + runAt: + runAt ?? + moment(new Date()) + .add(Math.pow(3, retryParams.retryCount ?? 1), 's') + .toDate(), + }); + appContextService.getLogger().info('Running task ' + taskId); + return taskId; + } +} + +export function createRetryTask( + taskInstance: ConcreteTaskInstance, + getDeps: () => Promise<{ esClient: ElasticsearchClient; soClient: SavedObjectsClient }>, + doRetry: ( + esClient: ElasticsearchClient, + soClient: SavedObjectsClient, + actionParams: ActionParams, + retryParams: RetryParams + ) => void +) { + return { + async run() { + appContextService.getLogger().info('Running bulk action retry task'); + + const { esClient, soClient } = await getDeps(); + + const retryParams = taskInstance.params.retryParams; + + appContextService + .getLogger() + .debug(`Retry #${retryParams.retryCount} of task ${taskInstance.id}`); + + if (retryParams.searchAfter) { + appContextService.getLogger().info('Continuing task from batch ' + retryParams.searchAfter); + } + + doRetry(esClient, soClient, taskInstance.params.actionParams, { + ...retryParams, + taskId: taskInstance.id, + }); + + appContextService.getLogger().info('Completed bulk action retry task'); + }, + + async cancel() {}, + }; +} diff --git a/x-pack/plugins/fleet/server/services/agents/crud.test.ts b/x-pack/plugins/fleet/server/services/agents/crud.test.ts index dbc2017c95cf52..c6d63b35ac7be2 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.test.ts @@ -9,7 +9,7 @@ import type { ElasticsearchClient } from '@kbn/core/server'; import type { Agent } from '../../types'; -import { errorsToResults, getAgentsByKuery, getAgentTags, processAgentsInBatches } from './crud'; +import { errorsToResults, getAgentsByKuery, getAgentTags } from './crud'; jest.mock('../../../common/services/is_agent_upgradeable', () => ({ isAgentUpgradeable: jest.fn().mockImplementation((agent: Agent) => agent.id.includes('up')), @@ -293,53 +293,6 @@ describe('Agents CRUD test', () => { }); }); - describe('processAgentsInBatches', () => { - const mockProcessAgents = (agents: Agent[]) => - Promise.resolve({ items: agents.map((agent) => ({ id: agent.id, success: true })) }); - it('should return results for multiple batches', async () => { - searchMock - .mockImplementationOnce(() => Promise.resolve(getEsResponse(['1', '2'], 3))) - .mockImplementationOnce(() => Promise.resolve(getEsResponse(['3'], 3))); - - const response = await processAgentsInBatches( - esClientMock, - { - kuery: 'active:true', - batchSize: 2, - showInactive: false, - }, - mockProcessAgents - ); - expect(response).toEqual({ - items: [ - { id: '1', success: true }, - { id: '2', success: true }, - { id: '3', success: true }, - ], - }); - }); - - it('should return results for one batch', async () => { - searchMock.mockImplementationOnce(() => Promise.resolve(getEsResponse(['1', '2', '3'], 3))); - - const response = await processAgentsInBatches( - esClientMock, - { - kuery: 'active:true', - showInactive: false, - }, - mockProcessAgents - ); - expect(response).toEqual({ - items: [ - { id: '1', success: true }, - { id: '2', success: true }, - { id: '3', success: true }, - ], - }); - }); - }); - describe('errorsToResults', () => { it('should transform errors to results', () => { const results = errorsToResults([{ id: '1' } as Agent, { id: '2' } as Agent], { diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 5b6e39c153b895..193bc71d04d292 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -4,12 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import Boom from '@hapi/boom'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { SortResults } from '@elastic/elasticsearch/lib/api/types'; import type { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server'; - import type { KueryNode } from '@kbn/es-query'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; @@ -254,55 +252,6 @@ export async function getAgentsByKuery( }; } -export async function processAgentsInBatches( - esClient: ElasticsearchClient, - options: Omit<ListWithKuery, 'page' | 'perPage'> & { - showInactive: boolean; - batchSize?: number; - }, - processAgents: ( - agents: Agent[], - includeSuccess: boolean - ) => Promise<{ items: BulkActionResult[] }> -): Promise<{ items: BulkActionResult[] }> { - const pitId = await openPointInTime(esClient); - - const perPage = options.batchSize ?? SO_SEARCH_LIMIT; - - const res = await getAgentsByKuery(esClient, { - ...options, - page: 1, - perPage, - pitId, - }); - - let currentAgents = res.agents; - // include successful agents if total agents does not exceed 10k - const skipSuccess = res.total > SO_SEARCH_LIMIT; - - let results = await processAgents(currentAgents, skipSuccess); - let allAgentsProcessed = currentAgents.length; - - while (allAgentsProcessed < res.total) { - const lastAgent = currentAgents[currentAgents.length - 1]; - const nextPage = await getAgentsByKuery(esClient, { - ...options, - page: 1, - perPage, - pitId, - searchAfter: lastAgent.sort!, - }); - currentAgents = nextPage.agents; - const currentResults = await processAgents(currentAgents, skipSuccess); - results = { items: results.items.concat(currentResults.items) }; - allAgentsProcessed += currentAgents.length; - } - - await closePointInTime(esClient, pitId); - - return results; -} - export function errorsToResults( agents: Agent[], errors: Record<Agent['id'], Error>, diff --git a/x-pack/plugins/fleet/server/services/agents/current_upgrades.ts b/x-pack/plugins/fleet/server/services/agents/current_upgrades.ts new file mode 100644 index 00000000000000..229074acde82b9 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/current_upgrades.ts @@ -0,0 +1,150 @@ +/* + * 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 { ElasticsearchClient } from '@kbn/core/server'; +import pMap from 'p-map'; + +import type { FleetServerAgentAction, CurrentUpgrade } from '../../types'; +import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../common'; +import { SO_SEARCH_LIMIT } from '../../constants'; + +/** + * Return current bulk upgrades (non completed or cancelled) + */ +export async function getCurrentBulkUpgrades( + esClient: ElasticsearchClient, + now = new Date().toISOString() +): Promise<CurrentUpgrade[]> { + // Fetch all non expired actions + const [_upgradeActions, cancelledActionIds] = await Promise.all([ + _getUpgradeActions(esClient, now), + _getCancelledActionId(esClient, now), + ]); + + let upgradeActions = _upgradeActions.filter( + (action) => cancelledActionIds.indexOf(action.actionId) < 0 + ); + + // Fetch acknowledged result for every upgrade action + upgradeActions = await pMap( + upgradeActions, + async (upgradeAction) => { + const { count } = await esClient.count({ + index: AGENT_ACTIONS_RESULTS_INDEX, + ignore_unavailable: true, + query: { + bool: { + must: [ + { + term: { + action_id: upgradeAction.actionId, + }, + }, + ], + }, + }, + }); + + return { + ...upgradeAction, + nbAgentsAck: count, + complete: upgradeAction.nbAgents <= count, + }; + }, + { concurrency: 20 } + ); + + upgradeActions = upgradeActions.filter((action) => !action.complete); + + return upgradeActions; +} + +async function _getCancelledActionId( + esClient: ElasticsearchClient, + now = new Date().toISOString() +) { + const res = await esClient.search<FleetServerAgentAction>({ + index: AGENT_ACTIONS_INDEX, + ignore_unavailable: true, + size: SO_SEARCH_LIMIT, + query: { + bool: { + must: [ + { + term: { + type: 'CANCEL', + }, + }, + { + exists: { + field: 'agents', + }, + }, + { + range: { + expiration: { gte: now }, + }, + }, + ], + }, + }, + }); + + return res.hits.hits.map((hit) => hit._source?.data?.target_id as string); +} + +async function _getUpgradeActions(esClient: ElasticsearchClient, now = new Date().toISOString()) { + const res = await esClient.search<FleetServerAgentAction>({ + index: AGENT_ACTIONS_INDEX, + ignore_unavailable: true, + size: SO_SEARCH_LIMIT, + query: { + bool: { + must: [ + { + term: { + type: 'UPGRADE', + }, + }, + { + exists: { + field: 'agents', + }, + }, + { + range: { + expiration: { gte: now }, + }, + }, + ], + }, + }, + }); + + return Object.values( + res.hits.hits.reduce((acc, hit) => { + if (!hit._source || !hit._source.action_id) { + return acc; + } + + if (!acc[hit._source.action_id]) { + acc[hit._source.action_id] = { + actionId: hit._source.action_id, + nbAgents: 0, + complete: false, + nbAgentsAck: 0, + version: hit._source.data?.version as string, + startTime: hit._source?.start_time, + }; + } + + acc[hit._source.action_id].nbAgents += hit._source.agents?.length ?? 0; + + return acc; + }, {} as { [k: string]: CurrentUpgrade }) + ); +} diff --git a/x-pack/plugins/fleet/server/services/agents/index.ts b/x-pack/plugins/fleet/server/services/agents/index.ts index 7712c614adbea0..302790cf6ae6d8 100644 --- a/x-pack/plugins/fleet/server/services/agents/index.ts +++ b/x-pack/plugins/fleet/server/services/agents/index.ts @@ -14,5 +14,8 @@ export * from './actions'; export * from './reassign'; export * from './setup'; export * from './update_agent_tags'; +export * from './action_status'; export { AgentServiceImpl } from './agent_service'; export type { AgentClient, AgentService } from './agent_service'; +export { BulkActionsResolver } from './bulk_actions_resolver'; +export { getCurrentBulkUpgrades } from './current_upgrades'; diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.ts b/x-pack/plugins/fleet/server/services/agents/reassign.ts index 1746d324d626ff..9889bc8a6eada2 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.ts @@ -12,18 +12,20 @@ import type { Agent, BulkActionResult } from '../../types'; import { agentPolicyService } from '../agent_policy'; import { AgentReassignmentError, HostedAgentPolicyRestrictionRelatedError } from '../../errors'; +import { SO_SEARCH_LIMIT } from '../../constants'; + import { getAgentDocuments, getAgentPolicyForAgent, updateAgent, - bulkUpdateAgents, - processAgentsInBatches, - errorsToResults, + getAgentsByKuery, + openPointInTime, } from './crud'; import type { GetAgentsOptions } from '.'; import { createAgentAction } from './actions'; import { searchHitToAgent } from './helpers'; -import { getHostedPolicies, isHostedAgent } from './hosted_agent'; + +import { ReassignActionRunner, reassignBatch } from './reassign_action_runner'; export async function reassignAgent( soClient: SavedObjectsClientContract, @@ -79,9 +81,12 @@ function isMgetDoc(doc?: estypes.MgetResponseItem<unknown>): doc is estypes.GetG export async function reassignAgents( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, - options: ({ agents: Agent[] } | GetAgentsOptions) & { force?: boolean; batchSize?: number }, + options: ({ agents: Agent[] } | GetAgentsOptions) & { + force?: boolean; + batchSize?: number; + }, newAgentPolicyId: string -): Promise<{ items: BulkActionResult[] }> { +): Promise<{ items: BulkActionResult[]; actionId?: string }> { const newAgentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); if (!newAgentPolicy) { throw Boom.notFound(`Agent policy not found: ${newAgentPolicyId}`); @@ -108,94 +113,37 @@ export async function reassignAgents( } } } else if ('kuery' in options) { - return await processAgentsInBatches( - esClient, - { - kuery: options.kuery, - showInactive: options.showInactive ?? false, - batchSize: options.batchSize, - }, - async (agents: Agent[], skipSuccess: boolean) => - await reassignBatch( - soClient, - esClient, + const batchSize = options.batchSize ?? SO_SEARCH_LIMIT; + const res = await getAgentsByKuery(esClient, { + kuery: options.kuery, + showInactive: options.showInactive ?? false, + page: 1, + perPage: batchSize, + }); + // running action in async mode for >10k agents (or actions > batchSize for testing purposes) + if (res.total <= batchSize) { + givenAgents = res.agents; + } else { + return await new ReassignActionRunner( + esClient, + soClient, + { + ...options, + batchSize, + total: res.total, newAgentPolicyId, - agents, - outgoingErrors, - undefined, - skipSuccess - ) - ); + }, + { pitId: await openPointInTime(esClient) } + ).runActionAsyncWithRetry(); + } } return await reassignBatch( soClient, esClient, - newAgentPolicyId, + { newAgentPolicyId }, givenAgents, outgoingErrors, 'agentIds' in options ? options.agentIds : undefined ); } - -async function reassignBatch( - soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient, - newAgentPolicyId: string, - givenAgents: Agent[], - outgoingErrors: Record<Agent['id'], Error>, - agentIds?: string[], - skipSuccess?: boolean -): Promise<{ items: BulkActionResult[] }> { - const errors: Record<Agent['id'], Error> = { ...outgoingErrors }; - - const hostedPolicies = await getHostedPolicies(soClient, givenAgents); - - // which are allowed to unenroll - const agentResults = await Promise.allSettled( - givenAgents.map(async (agent, index) => { - if (agent.policy_id === newAgentPolicyId) { - throw new AgentReassignmentError(`${agent.id} is already assigned to ${newAgentPolicyId}`); - } - - if (isHostedAgent(hostedPolicies, agent)) { - throw new HostedAgentPolicyRestrictionRelatedError( - `Cannot reassign an agent from hosted agent policy ${agent.policy_id}` - ); - } - - return agent; - }) - ); - - // Filter to agents that do not already use the new agent policy ID - const agentsToUpdate = agentResults.reduce<Agent[]>((agents, result, index) => { - if (result.status === 'fulfilled') { - agents.push(result.value); - } else { - const id = givenAgents[index].id; - errors[id] = result.reason; - } - return agents; - }, []); - - await bulkUpdateAgents( - esClient, - agentsToUpdate.map((agent) => ({ - agentId: agent.id, - data: { - policy_id: newAgentPolicyId, - policy_revision: null, - }, - })) - ); - - const now = new Date().toISOString(); - await createAgentAction(esClient, { - agents: agentsToUpdate.map((agent) => agent.id), - created_at: now, - type: 'POLICY_REASSIGN', - }); - - return { items: errorsToResults(givenAgents, errors, agentIds, skipSuccess) }; -} diff --git a/x-pack/plugins/fleet/server/services/agents/reassign_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/reassign_action_runner.ts new file mode 100644 index 00000000000000..c1bdb0e467c0da --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/reassign_action_runner.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 type { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server'; + +import type { Agent, BulkActionResult } from '../../types'; + +import { AgentReassignmentError, HostedAgentPolicyRestrictionRelatedError } from '../../errors'; + +import { appContextService } from '../app_context'; + +import { ActionRunner } from './action_runner'; + +import { errorsToResults, bulkUpdateAgents } from './crud'; +import { createAgentAction } from './actions'; +import { getHostedPolicies, isHostedAgent } from './hosted_agent'; +import { BulkActionTaskType } from './bulk_actions_resolver'; + +export class ReassignActionRunner extends ActionRunner { + protected async processAgents(agents: Agent[]): Promise<{ items: BulkActionResult[] }> { + return await reassignBatch( + this.soClient, + this.esClient, + this.actionParams! as any, + agents, + {}, + undefined, + true + ); + } + + protected getTaskType() { + return BulkActionTaskType.REASSIGN_RETRY; + } + + protected getActionType() { + return 'POLICY_REASSIGN'; + } +} + +export async function reassignBatch( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + options: { + newAgentPolicyId: string; + actionId?: string; + total?: number; + }, + givenAgents: Agent[], + outgoingErrors: Record<Agent['id'], Error>, + agentIds?: string[], + skipSuccess?: boolean +): Promise<{ items: BulkActionResult[] }> { + const errors: Record<Agent['id'], Error> = { ...outgoingErrors }; + + const hostedPolicies = await getHostedPolicies(soClient, givenAgents); + + const agentsToUpdate = givenAgents.reduce<Agent[]>((agents, agent) => { + if (agent.policy_id === options.newAgentPolicyId) { + errors[agent.id] = new AgentReassignmentError( + `${agent.id} is already assigned to ${options.newAgentPolicyId}` + ); + } else if (isHostedAgent(hostedPolicies, agent)) { + errors[agent.id] = new HostedAgentPolicyRestrictionRelatedError( + `Cannot reassign an agent from hosted agent policy ${agent.policy_id}` + ); + } else { + agents.push(agent); + } + return agents; + }, []); + + const result = { items: errorsToResults(givenAgents, errors, agentIds, skipSuccess) }; + + if (agentsToUpdate.length === 0) { + // early return if all agents failed validation + appContextService + .getLogger() + .debug('No agents to update, skipping agent update and action creation'); + return result; + } + + await bulkUpdateAgents( + esClient, + agentsToUpdate.map((agent) => ({ + agentId: agent.id, + data: { + policy_id: options.newAgentPolicyId, + policy_revision: null, + }, + })) + ); + + const now = new Date().toISOString(); + await createAgentAction(esClient, { + id: options.actionId, + agents: agentsToUpdate.map((agent) => agent.id), + created_at: now, + type: 'POLICY_REASSIGN', + total: options.total, + }); + + return result; +} diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts index 9ad39990b9ffa0..668ab79da691fa 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts @@ -12,7 +12,8 @@ import type { AgentPolicy } from '../../types'; import { HostedAgentPolicyRestrictionRelatedError } from '../../errors'; import { invalidateAPIKeys } from '../api_keys'; -import { invalidateAPIKeysForAgents, unenrollAgent, unenrollAgents } from './unenroll'; +import { unenrollAgent, unenrollAgents } from './unenroll'; +import { invalidateAPIKeysForAgents } from './unenroll_action_runner'; jest.mock('../api_keys'); diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.ts index d8a5b4b32a101b..941e1260894b46 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.ts @@ -8,21 +8,19 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import type { Agent, BulkActionResult } from '../../types'; -import { invalidateAPIKeys } from '../api_keys'; import { HostedAgentPolicyRestrictionRelatedError } from '../../errors'; +import { SO_SEARCH_LIMIT } from '../../constants'; import { createAgentAction } from './actions'; import type { GetAgentsOptions } from './crud'; -import { errorsToResults } from './crud'; +import { openPointInTime } from './crud'; +import { getAgentsByKuery } from './crud'; +import { getAgentById, getAgents, updateAgent, getAgentPolicyForAgent } from './crud'; import { - getAgentById, - getAgents, - updateAgent, - getAgentPolicyForAgent, - bulkUpdateAgents, - processAgentsInBatches, -} from './crud'; -import { getHostedPolicies, isHostedAgent } from './hosted_agent'; + invalidateAPIKeysForAgents, + UnenrollActionRunner, + unenrollBatch, +} from './unenroll_action_runner'; async function unenrollAgentIsAllowed( soClient: SavedObjectsClientContract, @@ -73,104 +71,33 @@ export async function unenrollAgents( revoke?: boolean; batchSize?: number; } -): Promise<{ items: BulkActionResult[] }> { +): Promise<{ items: BulkActionResult[]; actionId?: string }> { if ('agentIds' in options) { const givenAgents = await getAgents(esClient, options); return await unenrollBatch(soClient, esClient, givenAgents, options); } - return await processAgentsInBatches( - esClient, - { - kuery: options.kuery, - showInactive: options.showInactive ?? false, - batchSize: options.batchSize, - }, - async (agents: Agent[], skipSuccess?: boolean) => - await unenrollBatch(soClient, esClient, agents, options, skipSuccess) - ); -} -async function unenrollBatch( - soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient, - givenAgents: Agent[], - options: { - force?: boolean; - revoke?: boolean; - }, - skipSuccess?: boolean -): Promise<{ items: BulkActionResult[] }> { - // Filter to those not already unenrolled, or unenrolling - const agentsEnrolled = givenAgents.filter((agent) => { - if (options.revoke) { - return !agent.unenrolled_at; - } - return !agent.unenrollment_started_at && !agent.unenrolled_at; + const batchSize = options.batchSize ?? SO_SEARCH_LIMIT; + const res = await getAgentsByKuery(esClient, { + kuery: options.kuery, + showInactive: options.showInactive ?? false, + page: 1, + perPage: batchSize, }); - - const hostedPolicies = await getHostedPolicies(soClient, agentsEnrolled); - - const outgoingErrors: Record<Agent['id'], Error> = {}; - - // And which are allowed to unenroll - const agentsToUpdate = options.force - ? agentsEnrolled - : agentsEnrolled.reduce<Agent[]>((agents, agent, index) => { - if (isHostedAgent(hostedPolicies, agent)) { - const id = givenAgents[index].id; - outgoingErrors[id] = new HostedAgentPolicyRestrictionRelatedError( - `Cannot unenroll ${agent.id} from a hosted agent policy ${agent.policy_id}` - ); - } else { - agents.push(agent); - } - return agents; - }, []); - - const now = new Date().toISOString(); - if (options.revoke) { - // Get all API keys that need to be invalidated - await invalidateAPIKeysForAgents(agentsToUpdate); + if (res.total <= batchSize) { + const givenAgents = await getAgents(esClient, options); + return await unenrollBatch(soClient, esClient, givenAgents, options); } else { - // Create unenroll action for each agent - await createAgentAction(esClient, { - agents: agentsToUpdate.map((agent) => agent.id), - created_at: now, - type: 'UNENROLL', - }); - } - - // Update the necessary agents - const updateData = options.revoke - ? { unenrolled_at: now, active: false } - : { unenrollment_started_at: now }; - - await bulkUpdateAgents( - esClient, - agentsToUpdate.map(({ id }) => ({ agentId: id, data: updateData })) - ); - - return { - items: errorsToResults(givenAgents, outgoingErrors, undefined, skipSuccess), - }; -} - -export async function invalidateAPIKeysForAgents(agents: Agent[]) { - const apiKeys = agents.reduce<string[]>((keys, agent) => { - if (agent.access_api_key_id) { - keys.push(agent.access_api_key_id); - } - if (agent.default_api_key_id) { - keys.push(agent.default_api_key_id); - } - if (agent.default_api_key_history) { - agent.default_api_key_history.forEach((apiKey) => keys.push(apiKey.id)); - } - return keys; - }, []); - - if (apiKeys.length) { - await invalidateAPIKeys(apiKeys); + return await new UnenrollActionRunner( + esClient, + soClient, + { + ...options, + batchSize, + total: res.total, + }, + { pitId: await openPointInTime(esClient) } + ).runActionAsyncWithRetry(); } } diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/unenroll_action_runner.ts new file mode 100644 index 00000000000000..6eda4b00499e17 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/unenroll_action_runner.ts @@ -0,0 +1,122 @@ +/* + * 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 { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server'; + +import type { Agent, BulkActionResult } from '../../types'; + +import { HostedAgentPolicyRestrictionRelatedError } from '../../errors'; + +import { invalidateAPIKeys } from '../api_keys'; + +import { ActionRunner } from './action_runner'; + +import { errorsToResults, bulkUpdateAgents } from './crud'; +import { createAgentAction } from './actions'; +import { getHostedPolicies, isHostedAgent } from './hosted_agent'; +import { BulkActionTaskType } from './bulk_actions_resolver'; + +export class UnenrollActionRunner extends ActionRunner { + protected async processAgents(agents: Agent[]): Promise<{ items: BulkActionResult[] }> { + return await unenrollBatch(this.soClient, this.esClient, agents, this.actionParams!, true); + } + + protected getTaskType() { + return BulkActionTaskType.UNENROLL_RETRY; + } + + protected getActionType() { + return 'UNENROLL'; + } +} + +export async function unenrollBatch( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + givenAgents: Agent[], + options: { + force?: boolean; + revoke?: boolean; + actionId?: string; + total?: number; + }, + skipSuccess?: boolean +): Promise<{ items: BulkActionResult[] }> { + // Filter to those not already unenrolled, or unenrolling + const agentsEnrolled = givenAgents.filter((agent) => { + if (options.revoke) { + return !agent.unenrolled_at; + } + return !agent.unenrollment_started_at && !agent.unenrolled_at; + }); + + const hostedPolicies = await getHostedPolicies(soClient, agentsEnrolled); + + const outgoingErrors: Record<Agent['id'], Error> = {}; + + // And which are allowed to unenroll + const agentsToUpdate = options.force + ? agentsEnrolled + : agentsEnrolled.reduce<Agent[]>((agents, agent) => { + if (isHostedAgent(hostedPolicies, agent)) { + outgoingErrors[agent.id] = new HostedAgentPolicyRestrictionRelatedError( + `Cannot unenroll ${agent.id} from a hosted agent policy ${agent.policy_id}` + ); + } else { + agents.push(agent); + } + return agents; + }, []); + + const now = new Date().toISOString(); + if (options.revoke) { + // Get all API keys that need to be invalidated + await invalidateAPIKeysForAgents(agentsToUpdate); + } else { + // Create unenroll action for each agent + await createAgentAction(esClient, { + id: options.actionId, + agents: agentsToUpdate.map((agent) => agent.id), + created_at: now, + type: 'UNENROLL', + total: options.total, + }); + } + + // Update the necessary agents + const updateData = options.revoke + ? { unenrolled_at: now, active: false } + : { unenrollment_started_at: now }; + + await bulkUpdateAgents( + esClient, + agentsToUpdate.map(({ id }) => ({ agentId: id, data: updateData })) + ); + + return { + items: errorsToResults(givenAgents, outgoingErrors, undefined, skipSuccess), + }; +} + +export async function invalidateAPIKeysForAgents(agents: Agent[]) { + const apiKeys = agents.reduce<string[]>((keys, agent) => { + if (agent.access_api_key_id) { + keys.push(agent.access_api_key_id); + } + if (agent.default_api_key_id) { + keys.push(agent.default_api_key_id); + } + if (agent.default_api_key_history) { + agent.default_api_key_history.forEach((apiKey) => keys.push(apiKey.id)); + } + return keys; + }, []); + + if (apiKeys.length) { + await invalidateAPIKeys(apiKeys); + } +} diff --git a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts index 748f85db2843bd..7ac8e4e3f9e78a 100644 --- a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts @@ -16,6 +16,14 @@ jest.mock('./filter_hosted_agents', () => ({ .mockImplementation((soClient, givenAgents) => Promise.resolve(givenAgents)), })); +const mockRunAsync = jest.fn().mockResolvedValue({}); +jest.mock('./update_agent_tags_action_runner', () => ({ + ...jest.requireActual('./update_agent_tags_action_runner'), + UpdateAgentTagsActionRunner: jest.fn().mockImplementation(() => { + return { runActionAsyncWithRetry: mockRunAsync }; + }), +})); + describe('update_agent_tags', () => { let esClient: ElasticsearchClientMock; let soClient: jest.Mocked<SavedObjectsClientContract>; @@ -36,6 +44,8 @@ describe('update_agent_tags', () => { esClient.bulk.mockResolvedValue({ items: [], } as any); + + mockRunAsync.mockClear(); }); function expectTagsInEsBulk(tags: string[]) { @@ -114,4 +124,33 @@ describe('update_agent_tags', () => { expectTagsInEsBulk(['three', 'newName']); }); + + it('should run add tags async when actioning more agents than batch size', async () => { + esClient.search.mockResolvedValue({ + hits: { + total: 3, + hits: [ + { + _id: 'agent1', + _source: {}, + } as any, + { + _id: 'agent2', + _source: {}, + } as any, + { + _id: 'agent3', + _source: {}, + } as any, + ], + }, + took: 0, + timed_out: false, + _shards: {} as any, + }); + + await updateAgentTags(soClient, esClient, { kuery: '', batchSize: 2 }, ['newName'], []); + + expect(mockRunAsync).toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts index c4893c77696516..4496e16cbc4766 100644 --- a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts +++ b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts @@ -5,22 +5,18 @@ * 2.0. */ -import { difference, uniq } from 'lodash'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import type { Agent, BulkActionResult } from '../../types'; import { AgentReassignmentError } from '../../errors'; -import { - getAgentDocuments, - bulkUpdateAgents, - processAgentsInBatches, - errorsToResults, -} from './crud'; +import { SO_SEARCH_LIMIT } from '../../constants'; + +import { getAgentDocuments, getAgentsByKuery, openPointInTime } from './crud'; import type { GetAgentsOptions } from '.'; import { searchHitToAgent } from './helpers'; -import { filterHostedPolicies } from './filter_hosted_agents'; +import { UpdateAgentTagsActionRunner, updateTagsBatch } from './update_agent_tags_action_runner'; function isMgetDoc(doc?: estypes.MgetResponseItem<unknown>): doc is estypes.GetGetResult { return Boolean(doc && 'found' in doc); @@ -32,9 +28,9 @@ export async function updateAgentTags( options: ({ agents: Agent[] } | GetAgentsOptions) & { batchSize?: number }, tagsToAdd: string[], tagsToRemove: string[] -) { +): Promise<{ items: BulkActionResult[]; actionId?: string }> { const outgoingErrors: Record<Agent['id'], Error> = {}; - const givenAgents: Agent[] = []; + let givenAgents: Agent[] = []; if ('agentIds' in options) { const givenAgentsResults = await getAgentDocuments(esClient, options.agentIds); @@ -48,25 +44,29 @@ export async function updateAgentTags( } } } else if ('kuery' in options) { - return await processAgentsInBatches( - esClient, - { - kuery: options.kuery, - showInactive: true, - batchSize: options.batchSize, - }, - async (agents: Agent[], skipSuccess: boolean) => - await updateTagsBatch( - soClient, - esClient, - agents, - outgoingErrors, + const batchSize = options.batchSize ?? SO_SEARCH_LIMIT; + const res = await getAgentsByKuery(esClient, { + kuery: options.kuery, + showInactive: options.showInactive ?? false, + page: 1, + perPage: batchSize, + }); + if (res.total <= batchSize) { + givenAgents = res.agents; + } else { + return await new UpdateAgentTagsActionRunner( + esClient, + soClient, + { + ...options, + batchSize, + total: res.total, tagsToAdd, tagsToRemove, - undefined, - skipSuccess - ) - ); + }, + { pitId: await openPointInTime(esClient) } + ).runActionAsyncWithRetry(); + } } return await updateTagsBatch( @@ -74,57 +74,7 @@ export async function updateAgentTags( esClient, givenAgents, outgoingErrors, - tagsToAdd, - tagsToRemove, + { tagsToAdd, tagsToRemove }, 'agentIds' in options ? options.agentIds : undefined ); } - -async function updateTagsBatch( - soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient, - givenAgents: Agent[], - outgoingErrors: Record<Agent['id'], Error>, - tagsToAdd: string[], - tagsToRemove: string[], - agentIds?: string[], - skipSuccess?: boolean -): Promise<{ items: BulkActionResult[] }> { - const errors: Record<Agent['id'], Error> = { ...outgoingErrors }; - - const filteredAgents = await filterHostedPolicies( - soClient, - givenAgents, - errors, - `Cannot modify tags on a hosted agent` - ); - - const getNewTags = (agent: Agent): string[] => { - const existingTags = agent.tags ?? []; - - if (tagsToAdd.length === 1 && tagsToRemove.length === 1) { - const removableTagIndex = existingTags.indexOf(tagsToRemove[0]); - if (removableTagIndex > -1) { - const newTags = uniq([ - ...existingTags.slice(0, removableTagIndex), - tagsToAdd[0], - ...existingTags.slice(removableTagIndex + 1), - ]); - return newTags; - } - } - return uniq(difference(existingTags, tagsToRemove).concat(tagsToAdd)); - }; - - await bulkUpdateAgents( - esClient, - filteredAgents.map((agent) => ({ - agentId: agent.id, - data: { - tags: getNewTags(agent), - }, - })) - ); - - return { items: errorsToResults(filteredAgents, errors, agentIds, skipSuccess) }; -} diff --git a/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts new file mode 100644 index 00000000000000..906566aee9f413 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts @@ -0,0 +1,91 @@ +/* + * 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 { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server'; + +import { difference, uniq } from 'lodash'; + +import type { Agent, BulkActionResult } from '../../types'; + +import { ActionRunner } from './action_runner'; + +import { errorsToResults, bulkUpdateAgents } from './crud'; +import { BulkActionTaskType } from './bulk_actions_resolver'; +import { filterHostedPolicies } from './filter_hosted_agents'; + +export class UpdateAgentTagsActionRunner extends ActionRunner { + protected async processAgents(agents: Agent[]): Promise<{ items: BulkActionResult[] }> { + return await updateTagsBatch( + this.soClient, + this.esClient, + agents, + {}, + { tagsToAdd: this.actionParams?.tagsToAdd, tagsToRemove: this.actionParams?.tagsToRemove }, + undefined, + true + ); + } + + protected getTaskType() { + return BulkActionTaskType.UPDATE_AGENT_TAGS_RETRY; + } + + protected getActionType() { + return 'UPDATE_TAGS'; + } +} + +export async function updateTagsBatch( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + givenAgents: Agent[], + outgoingErrors: Record<Agent['id'], Error>, + options: { + tagsToAdd: string[]; + tagsToRemove: string[]; + }, + agentIds?: string[], + skipSuccess?: boolean +): Promise<{ items: BulkActionResult[] }> { + const errors: Record<Agent['id'], Error> = { ...outgoingErrors }; + + const filteredAgents = await filterHostedPolicies( + soClient, + givenAgents, + errors, + `Cannot modify tags on a hosted agent` + ); + + const getNewTags = (agent: Agent): string[] => { + const existingTags = agent.tags ?? []; + + if (options.tagsToAdd.length === 1 && options.tagsToRemove.length === 1) { + const removableTagIndex = existingTags.indexOf(options.tagsToRemove[0]); + if (removableTagIndex > -1) { + const newTags = uniq([ + ...existingTags.slice(0, removableTagIndex), + options.tagsToAdd[0], + ...existingTags.slice(removableTagIndex + 1), + ]); + return newTags; + } + } + return uniq(difference(existingTags, options.tagsToRemove).concat(options.tagsToAdd)); + }; + + await bulkUpdateAgents( + esClient, + filteredAgents.map((agent) => ({ + agentId: agent.id, + data: { + tags: getNewTags(agent), + }, + })) + ); + + return { items: errorsToResults(filteredAgents, errors, agentIds, skipSuccess) }; +} diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index 1083e8f728ee19..b6c50a3b5dc3c7 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -6,28 +6,18 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; -import moment from 'moment'; -import pMap from 'p-map'; -import uuid from 'uuid/v4'; -import type { Agent, BulkActionResult, FleetServerAgentAction, CurrentUpgrade } from '../../types'; -import { - AgentReassignmentError, - HostedAgentPolicyRestrictionRelatedError, - IngestManagerError, -} from '../../errors'; -import { isAgentUpgradeable } from '../../../common/services'; -import { appContextService } from '../app_context'; -import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../common'; +import type { Agent, BulkActionResult } from '../../types'; +import { AgentReassignmentError, HostedAgentPolicyRestrictionRelatedError } from '../../errors'; +import { SO_SEARCH_LIMIT } from '../../constants'; import { createAgentAction } from './actions'; import type { GetAgentsOptions } from './crud'; -import { errorsToResults, processAgentsInBatches } from './crud'; -import { getAgentDocuments, updateAgent, bulkUpdateAgents, getAgentPolicyForAgent } from './crud'; +import { openPointInTime } from './crud'; +import { getAgentsByKuery } from './crud'; +import { getAgentDocuments, updateAgent, getAgentPolicyForAgent } from './crud'; import { searchHitToAgent } from './helpers'; -import { getHostedPolicies, isHostedAgent } from './hosted_agent'; - -const MINIMUM_EXECUTION_DURATION_SECONDS = 1800; // 30m +import { UpgradeActionRunner, upgradeBatch } from './upgrade_action_runner'; function isMgetDoc(doc?: estypes.MgetResponseItem<unknown>): doc is estypes.GetGetResult { return Boolean(doc && 'found' in doc); @@ -83,7 +73,7 @@ export async function sendUpgradeAgentsActions( startTime?: string; batchSize?: number; } -) { +): Promise<{ items: BulkActionResult[]; actionId?: string }> { // Full set of agents const outgoingErrors: Record<Agent['id'], Error> = {}; let givenAgents: Agent[] = []; @@ -101,286 +91,28 @@ export async function sendUpgradeAgentsActions( } } } else if ('kuery' in options) { - const actionId = uuid(); - return await processAgentsInBatches( - esClient, - { - kuery: options.kuery, - showInactive: options.showInactive ?? false, - batchSize: options.batchSize, - }, - async (agents: Agent[], skipSuccess: boolean) => - await upgradeBatch( - soClient, - esClient, - agents, - outgoingErrors, - { ...options, actionId }, - skipSuccess - ) - ); - } - - return await upgradeBatch(soClient, esClient, givenAgents, outgoingErrors, options); -} - -async function upgradeBatch( - soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient, - givenAgents: Agent[], - outgoingErrors: Record<Agent['id'], Error>, - options: ({ agents: Agent[] } | GetAgentsOptions) & { - actionId?: string; - version: string; - sourceUri?: string | undefined; - force?: boolean; - upgradeDurationSeconds?: number; - startTime?: string; - }, - skipSuccess?: boolean -): Promise<{ items: BulkActionResult[] }> { - const errors: Record<Agent['id'], Error> = { ...outgoingErrors }; - - const hostedPolicies = await getHostedPolicies(soClient, givenAgents); - - // results from getAgents with options.kuery '' (or even 'active:false') may include hosted agents - // filter them out unless options.force - const agentsToCheckUpgradeable = - 'kuery' in options && !options.force - ? givenAgents.filter((agent: Agent) => !isHostedAgent(hostedPolicies, agent)) - : givenAgents; - - const kibanaVersion = appContextService.getKibanaVersion(); - const upgradeableResults = await Promise.allSettled( - agentsToCheckUpgradeable.map(async (agent) => { - // Filter out agents currently unenrolling, unenrolled, or not upgradeable b/c of version check - const isNotAllowed = - !options.force && !isAgentUpgradeable(agent, kibanaVersion, options.version); - if (isNotAllowed) { - throw new IngestManagerError(`${agent.id} is not upgradeable`); - } - - if (!options.force && isHostedAgent(hostedPolicies, agent)) { - throw new HostedAgentPolicyRestrictionRelatedError( - `Cannot upgrade agent in hosted agent policy ${agent.policy_id}` - ); - } - return agent; - }) - ); - - // Filter & record errors from results - const agentsToUpdate = upgradeableResults.reduce<Agent[]>((agents, result, index) => { - if (result.status === 'fulfilled') { - agents.push(result.value); + const batchSize = options.batchSize ?? SO_SEARCH_LIMIT; + const res = await getAgentsByKuery(esClient, { + kuery: options.kuery, + showInactive: options.showInactive ?? false, + page: 1, + perPage: batchSize, + }); + if (res.total <= batchSize) { + givenAgents = res.agents; } else { - const id = givenAgents[index].id; - errors[id] = result.reason; - } - return agents; - }, []); - - // Create upgrade action for each agent - const now = new Date().toISOString(); - const data = { - version: options.version, - source_uri: options.sourceUri, - }; - - const rollingUpgradeOptions = getRollingUpgradeOptions( - options?.startTime, - options.upgradeDurationSeconds - ); - - await createAgentAction(esClient, { - id: options.actionId, - created_at: now, - data, - ack_data: data, - type: 'UPGRADE', - agents: agentsToUpdate.map((agent) => agent.id), - ...rollingUpgradeOptions, - }); - - await bulkUpdateAgents( - esClient, - agentsToUpdate.map((agent) => ({ - agentId: agent.id, - data: { - upgrade_started_at: now, - upgrade_status: 'started', - }, - })) - ); - - return { - items: errorsToResults( - givenAgents, - errors, - 'agentIds' in options ? options.agentIds : undefined, - skipSuccess - ), - }; -} - -/** - * Return current bulk upgrades (non completed or cancelled) - */ -export async function getCurrentBulkUpgrades( - esClient: ElasticsearchClient, - now = new Date().toISOString() -): Promise<CurrentUpgrade[]> { - // Fetch all non expired actions - const [_upgradeActions, cancelledActionIds] = await Promise.all([ - _getUpgradeActions(esClient, now), - _getCancelledActionId(esClient, now), - ]); - - let upgradeActions = _upgradeActions.filter( - (action) => cancelledActionIds.indexOf(action.actionId) < 0 - ); - - // Fetch acknowledged result for every upgrade action - upgradeActions = await pMap( - upgradeActions, - async (upgradeAction) => { - const { count } = await esClient.count({ - index: AGENT_ACTIONS_RESULTS_INDEX, - ignore_unavailable: true, - query: { - bool: { - must: [ - { - term: { - action_id: upgradeAction.actionId, - }, - }, - ], - }, + return await new UpgradeActionRunner( + esClient, + soClient, + { + ...options, + batchSize, + total: res.total, }, - }); - - return { - ...upgradeAction, - nbAgentsAck: count, - complete: upgradeAction.nbAgents <= count, - }; - }, - { concurrency: 20 } - ); - - upgradeActions = upgradeActions.filter((action) => !action.complete); - - return upgradeActions; -} - -async function _getCancelledActionId( - esClient: ElasticsearchClient, - now = new Date().toISOString() -) { - const res = await esClient.search<FleetServerAgentAction>({ - index: AGENT_ACTIONS_INDEX, - ignore_unavailable: true, - query: { - bool: { - must: [ - { - term: { - type: 'CANCEL', - }, - }, - { - exists: { - field: 'agents', - }, - }, - { - range: { - expiration: { gte: now }, - }, - }, - ], - }, - }, - }); - - return res.hits.hits.map((hit) => hit._source?.data?.target_id as string); -} - -async function _getUpgradeActions(esClient: ElasticsearchClient, now = new Date().toISOString()) { - const res = await esClient.search<FleetServerAgentAction>({ - index: AGENT_ACTIONS_INDEX, - ignore_unavailable: true, - query: { - bool: { - must: [ - { - term: { - type: 'UPGRADE', - }, - }, - { - exists: { - field: 'agents', - }, - }, - { - range: { - expiration: { gte: now }, - }, - }, - ], - }, - }, - }); - - return Object.values( - res.hits.hits.reduce((acc, hit) => { - if (!hit._source || !hit._source.action_id) { - return acc; - } - - if (!acc[hit._source.action_id]) { - acc[hit._source.action_id] = { - actionId: hit._source.action_id, - nbAgents: 0, - complete: false, - nbAgentsAck: 0, - version: hit._source.data?.version as string, - startTime: hit._source?.start_time, - }; - } - - acc[hit._source.action_id].nbAgents += hit._source.agents?.length ?? 0; + { pitId: await openPointInTime(esClient) } + ).runActionAsyncWithRetry(); + } + } - return acc; - }, {} as { [k: string]: CurrentUpgrade }) - ); + return await upgradeBatch(soClient, esClient, givenAgents, outgoingErrors, options); } - -const getRollingUpgradeOptions = (startTime?: string, upgradeDurationSeconds?: number) => { - const now = new Date().toISOString(); - // Perform a rolling upgrade - if (upgradeDurationSeconds) { - return { - start_time: startTime ?? now, - minimum_execution_duration: MINIMUM_EXECUTION_DURATION_SECONDS, - expiration: moment(startTime ?? now) - .add(upgradeDurationSeconds, 'seconds') - .toISOString(), - }; - } - // Schedule without rolling upgrade (Immediately after start_time) - if (startTime && !upgradeDurationSeconds) { - return { - start_time: startTime ?? now, - minimum_execution_duration: MINIMUM_EXECUTION_DURATION_SECONDS, - expiration: moment(startTime) - .add(MINIMUM_EXECUTION_DURATION_SECONDS, 'seconds') - .toISOString(), - }; - } else { - // Regular bulk upgrade (non scheduled, non rolling) - return {}; - } -}; diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts new file mode 100644 index 00000000000000..ca2bd6a996d678 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts @@ -0,0 +1,180 @@ +/* + * 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 { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server'; + +import moment from 'moment'; + +import { isAgentUpgradeable } from '../../../common/services'; + +import type { Agent, BulkActionResult } from '../../types'; + +import { HostedAgentPolicyRestrictionRelatedError, IngestManagerError } from '../../errors'; + +import { appContextService } from '../app_context'; + +import { ActionRunner } from './action_runner'; + +import type { GetAgentsOptions } from './crud'; +import { errorsToResults, bulkUpdateAgents } from './crud'; +import { createAgentAction } from './actions'; +import { getHostedPolicies, isHostedAgent } from './hosted_agent'; +import { BulkActionTaskType } from './bulk_actions_resolver'; + +export class UpgradeActionRunner extends ActionRunner { + protected async processAgents(agents: Agent[]): Promise<{ items: BulkActionResult[] }> { + return await upgradeBatch( + this.soClient, + this.esClient, + agents, + {}, + this.actionParams! as any, + true + ); + } + + protected getTaskType() { + return BulkActionTaskType.UPGRADE_RETRY; + } + + protected getActionType() { + return 'UPGRADE'; + } +} + +export async function upgradeBatch( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + givenAgents: Agent[], + outgoingErrors: Record<Agent['id'], Error>, + options: ({ agents: Agent[] } | GetAgentsOptions) & { + actionId?: string; + version: string; + sourceUri?: string | undefined; + force?: boolean; + upgradeDurationSeconds?: number; + startTime?: string; + total?: number; + }, + skipSuccess?: boolean +): Promise<{ items: BulkActionResult[] }> { + const errors: Record<Agent['id'], Error> = { ...outgoingErrors }; + + const hostedPolicies = await getHostedPolicies(soClient, givenAgents); + + // results from getAgents with options.kuery '' (or even 'active:false') may include hosted agents + // filter them out unless options.force + const agentsToCheckUpgradeable = + 'kuery' in options && !options.force + ? givenAgents.filter((agent: Agent) => !isHostedAgent(hostedPolicies, agent)) + : givenAgents; + + const kibanaVersion = appContextService.getKibanaVersion(); + const upgradeableResults = await Promise.allSettled( + agentsToCheckUpgradeable.map(async (agent) => { + // Filter out agents currently unenrolling, unenrolled, or not upgradeable b/c of version check + const isNotAllowed = + !options.force && !isAgentUpgradeable(agent, kibanaVersion, options.version); + if (isNotAllowed) { + throw new IngestManagerError(`${agent.id} is not upgradeable`); + } + + if (!options.force && isHostedAgent(hostedPolicies, agent)) { + throw new HostedAgentPolicyRestrictionRelatedError( + `Cannot upgrade agent in hosted agent policy ${agent.policy_id}` + ); + } + return agent; + }) + ); + + // Filter & record errors from results + const agentsToUpdate = upgradeableResults.reduce<Agent[]>((agents, result, index) => { + if (result.status === 'fulfilled') { + agents.push(result.value); + } else { + const id = givenAgents[index].id; + errors[id] = result.reason; + } + return agents; + }, []); + + // Create upgrade action for each agent + const now = new Date().toISOString(); + const data = { + version: options.version, + source_uri: options.sourceUri, + }; + + const rollingUpgradeOptions = getRollingUpgradeOptions( + options?.startTime, + options.upgradeDurationSeconds + ); + + await createAgentAction(esClient, { + id: options.actionId, + created_at: now, + data, + ack_data: data, + type: 'UPGRADE', + total: options.total, + agents: agentsToUpdate.map((agent) => agent.id), + ...rollingUpgradeOptions, + }); + + await bulkUpdateAgents( + esClient, + agentsToUpdate.map((agent) => ({ + agentId: agent.id, + data: { + upgrade_started_at: now, + upgrade_status: 'started', + }, + })) + ); + + return { + items: errorsToResults( + givenAgents, + errors, + 'agentIds' in options ? options.agentIds : undefined, + skipSuccess + ), + }; +} + +const MINIMUM_EXECUTION_DURATION_SECONDS = 60 * 60 * 2; // 2h + +const getRollingUpgradeOptions = (startTime?: string, upgradeDurationSeconds?: number) => { + const now = new Date().toISOString(); + // Perform a rolling upgrade + if (upgradeDurationSeconds) { + return { + start_time: startTime ?? now, + minimum_execution_duration: Math.min( + MINIMUM_EXECUTION_DURATION_SECONDS, + upgradeDurationSeconds + ), + expiration: moment(startTime ?? now) + .add(upgradeDurationSeconds, 'seconds') + .toISOString(), + }; + } + // Schedule without rolling upgrade (Immediately after start_time) + if (startTime && !upgradeDurationSeconds) { + return { + start_time: startTime ?? now, + minimum_execution_duration: MINIMUM_EXECUTION_DURATION_SECONDS, + expiration: moment(startTime) + .add(MINIMUM_EXECUTION_DURATION_SECONDS, 'seconds') + .toISOString(), + }; + } else { + // Regular bulk upgrade (non scheduled, non rolling) + return {}; + } +}; diff --git a/x-pack/plugins/fleet/server/services/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index 86f9408bef3f3f..3257f3e969ec80 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -41,6 +41,8 @@ import type { import type { FleetAppContext } from '../plugin'; import type { TelemetryEventsSender } from '../telemetry/sender'; +import type { BulkActionsResolver } from './agents'; + class AppContextService { private encryptedSavedObjects: EncryptedSavedObjectsClient | undefined; private encryptedSavedObjectsSetup: EncryptedSavedObjectsPluginSetup | undefined; @@ -61,6 +63,7 @@ class AppContextService { private externalCallbacks: ExternalCallbacksStorage = new Map(); private telemetryEventsSender: TelemetryEventsSender | undefined; private savedObjectsTagging: SavedObjectTaggingStart | undefined; + private bulkActionsResolver: BulkActionsResolver | undefined; public start(appContext: FleetAppContext) { this.data = appContext.data; @@ -79,6 +82,7 @@ class AppContextService { this.httpSetup = appContext.httpSetup; this.telemetryEventsSender = appContext.telemetryEventsSender; this.savedObjectsTagging = appContext.savedObjectsTagging; + this.bulkActionsResolver = appContext.bulkActionsResolver; if (appContext.config$) { this.config$ = appContext.config$; @@ -228,6 +232,10 @@ class AppContextService { public getTelemetryEventsSender() { return this.telemetryEventsSender; } + + public getBulkActionsResolver() { + return this.bulkActionsResolver; + } } export const appContextService = new AppContextService(); diff --git a/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts index 90b2aa851fd522..f05441f3db1ff7 100644 --- a/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts +++ b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts @@ -6,7 +6,7 @@ */ export const elasticAgentStandaloneManifest = `--- -# For more information refer to https://www.elastic.co/guide/en/fleet/current/running-on-kubernetes-standalone.html +# For more information refer https://www.elastic.co/guide/en/fleet/current/running-on-kubernetes-standalone.html apiVersion: apps/v1 kind: DaemonSet metadata: diff --git a/x-pack/plugins/fleet/server/services/epm/packages/update.ts b/x-pack/plugins/fleet/server/services/epm/packages/update.ts index 4ab47954f51ca6..224c9332fad62a 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/update.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/update.ts @@ -8,6 +8,8 @@ import type { SavedObjectsClientContract } from '@kbn/core/server'; import type { TypeOf } from '@kbn/config-schema'; +import type { ExperimentalIndexingFeature } from '../../../../common/types'; + import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import type { Installation, UpdatePackageRequestSchema } from '../../../types'; import { IngestManagerError } from '../../../errors'; @@ -40,3 +42,21 @@ export async function updatePackage( return packageInfo; } + +export async function updateDatastreamExperimentalFeatures( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + dataStreamFeatureMapping: Array<{ + data_stream: string; + features: Record<ExperimentalIndexingFeature, boolean>; + }> +) { + await savedObjectsClient.update<Installation>( + PACKAGES_SAVED_OBJECT_TYPE, + pkgName, + { + experimental_data_stream_features: dataStreamFeatureMapping, + }, + { refresh: 'wait_for' } + ); +} diff --git a/x-pack/plugins/fleet/server/services/package_policies/__snapshots__/simplified_package_policy_helper.test.ts.snap b/x-pack/plugins/fleet/server/services/package_policies/__snapshots__/simplified_package_policy_helper.test.ts.snap index 200eed919407ed..35c8b917853f9e 100644 --- a/x-pack/plugins/fleet/server/services/package_policies/__snapshots__/simplified_package_policy_helper.test.ts.snap +++ b/x-pack/plugins/fleet/server/services/package_policies/__snapshots__/simplified_package_policy_helper.test.ts.snap @@ -15,6 +15,7 @@ Object { "type": "logs", }, "enabled": true, + "release": "ga", "vars": Object { "ignore_older": Object { "type": "text", @@ -48,6 +49,7 @@ Object { "type": "logs", }, "enabled": true, + "release": "ga", "vars": Object { "ignore_older": Object { "type": "text", @@ -89,6 +91,7 @@ Object { "type": "logs", }, "enabled": false, + "release": "ga", "vars": Object { "interval": Object { "type": "text", @@ -121,6 +124,7 @@ Object { "type": "logs", }, "enabled": false, + "release": "ga", "vars": Object { "interval": Object { "type": "text", @@ -203,6 +207,7 @@ Object { "type": "metrics", }, "enabled": true, + "release": "ga", "vars": Object { "period": Object { "type": "text", diff --git a/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.test.ts b/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.test.ts new file mode 100644 index 00000000000000..8c7afa5d30fed9 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.test.ts @@ -0,0 +1,212 @@ +/* + * 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 { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; +import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks'; + +import type { NewPackagePolicy, PackagePolicy } from '../../types'; + +import { handleExperimentalDatastreamFeatureOptIn } from './experimental_datastream_features'; + +function getNewTestPackagePolicy({ + isSyntheticSourceEnabled, +}: { + isSyntheticSourceEnabled: boolean; +}): NewPackagePolicy { + const packagePolicy: NewPackagePolicy = { + name: 'Test policy', + policy_id: 'agent-policy', + description: 'Test policy description', + namespace: 'default', + enabled: true, + inputs: [], + package: { + name: 'test', + title: 'Test', + version: '0.0.1', + experimental_data_stream_features: [ + { + data_stream: 'metrics-test.test', + features: { + synthetic_source: isSyntheticSourceEnabled, + }, + }, + ], + }, + }; + + return packagePolicy; +} + +function getExistingTestPackagePolicy({ + isSyntheticSourceEnabled, +}: { + isSyntheticSourceEnabled: boolean; +}): PackagePolicy { + const packagePolicy: PackagePolicy = { + id: 'test-policy', + name: 'Test policy', + policy_id: 'agent-policy', + description: 'Test policy description', + namespace: 'default', + enabled: true, + inputs: [], + package: { + name: 'test', + title: 'Test', + version: '0.0.1', + experimental_data_stream_features: [ + { + data_stream: 'metrics-test.test', + features: { + synthetic_source: isSyntheticSourceEnabled, + }, + }, + ], + }, + revision: 1, + created_by: 'system', + created_at: '2022-01-01T00:00:00.000Z', + updated_by: 'system', + updated_at: '2022-01-01T00:00:00.000Z', + }; + + return packagePolicy; +} + +describe('experimental_datastream_features', () => { + beforeEach(() => { + soClient.get.mockClear(); + esClient.cluster.getComponentTemplate.mockClear(); + esClient.cluster.putComponentTemplate.mockClear(); + }); + + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + describe('when package policy does not exist (create)', () => { + it('updates component template', async () => { + const packagePolicy = getNewTestPackagePolicy({ isSyntheticSourceEnabled: true }); + + soClient.get.mockResolvedValueOnce({ + attributes: { + experimental_data_stream_features: [ + { data_stream: 'metrics-test.test', features: { synthetic_source: false } }, + ], + }, + id: 'mocked', + type: 'mocked', + references: [], + }); + + esClient.cluster.getComponentTemplate.mockResolvedValueOnce({ + component_templates: [ + { + name: 'metrics-test.test@package', + component_template: { + template: { + settings: {}, + mappings: { + _source: { + // @ts-expect-error + mode: 'stored', + }, + }, + }, + }, + }, + ], + }); + + await handleExperimentalDatastreamFeatureOptIn({ soClient, esClient, packagePolicy }); + + expect(esClient.cluster.getComponentTemplate).toHaveBeenCalled(); + expect(esClient.cluster.putComponentTemplate).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + template: expect.objectContaining({ + mappings: expect.objectContaining({ _source: { mode: 'synthetic' } }), + }), + }), + }) + ); + }); + }); + + describe('when package policy exists (update)', () => { + describe('when opt in status in unchanged', () => { + it('does not update component template', async () => { + const packagePolicy = getExistingTestPackagePolicy({ isSyntheticSourceEnabled: true }); + + soClient.get.mockResolvedValueOnce({ + attributes: { + experimental_data_stream_features: [ + { data_stream: 'metrics-test.test', features: { synthetic_source: true } }, + ], + }, + id: 'mocked', + type: 'mocked', + references: [], + }); + + await handleExperimentalDatastreamFeatureOptIn({ soClient, esClient, packagePolicy }); + + expect(esClient.cluster.getComponentTemplate).not.toHaveBeenCalled(); + expect(esClient.cluster.putComponentTemplate).not.toHaveBeenCalled(); + }); + }); + + describe('when opt in status is changed', () => { + it('updates component template', async () => { + const packagePolicy = getExistingTestPackagePolicy({ isSyntheticSourceEnabled: true }); + + soClient.get.mockResolvedValueOnce({ + attributes: { + experimental_data_stream_features: [ + { data_stream: 'metrics-test.test', features: { synthetic_source: false } }, + ], + }, + id: 'mocked', + type: 'mocked', + references: [], + }); + + esClient.cluster.getComponentTemplate.mockResolvedValueOnce({ + component_templates: [ + { + name: 'metrics-test.test@package', + component_template: { + template: { + settings: {}, + mappings: { + _source: { + // @ts-expect-error + mode: 'stored', + }, + }, + }, + }, + }, + ], + }); + + await handleExperimentalDatastreamFeatureOptIn({ soClient, esClient, packagePolicy }); + + expect(esClient.cluster.getComponentTemplate).toHaveBeenCalled(); + expect(esClient.cluster.putComponentTemplate).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + template: expect.objectContaining({ + mappings: expect.objectContaining({ _source: { mode: 'synthetic' } }), + }), + }), + }) + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.ts b/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.ts new file mode 100644 index 00000000000000..2b8b05aed89c37 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.ts @@ -0,0 +1,88 @@ +/* + * 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 { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; + +import type { NewPackagePolicy, PackagePolicy } from '../../types'; +import { getInstallation } from '../epm/packages'; +import { updateDatastreamExperimentalFeatures } from '../epm/packages/update'; + +export async function handleExperimentalDatastreamFeatureOptIn({ + soClient, + esClient, + packagePolicy, +}: { + soClient: SavedObjectsClientContract; + esClient: ElasticsearchClient; + packagePolicy: PackagePolicy | NewPackagePolicy; +}) { + if (!packagePolicy.package?.experimental_data_stream_features) { + return; + } + + // If we're performing an update, we want to check if we actually need to perform + // an update to the component templates for the package. So we fetch the saved object + // for the package policy here to compare later. + let installation; + + if (packagePolicy.package) { + installation = await getInstallation({ + savedObjectsClient: soClient, + pkgName: packagePolicy.package.name, + }); + } + + for (const featureMapEntry of packagePolicy.package.experimental_data_stream_features) { + const existingOptIn = installation?.experimental_data_stream_features?.find( + (optIn) => optIn.data_stream === featureMapEntry.data_stream + ); + + const isOptInChanged = + existingOptIn?.features.synthetic_source !== featureMapEntry.features.synthetic_source; + + // If the feature opt-in status in unchanged, we don't need to update any component templates + if (!isOptInChanged) { + continue; + } + + const componentTemplateName = `${featureMapEntry.data_stream}@package`; + const componentTemplateRes = await esClient.cluster.getComponentTemplate({ + name: componentTemplateName, + }); + + const componentTemplate = componentTemplateRes.component_templates[0].component_template; + + const body = { + template: { + ...componentTemplate.template, + mappings: { + ...componentTemplate.template.mappings, + _source: { + mode: featureMapEntry.features.synthetic_source ? 'synthetic' : 'stored', + }, + }, + }, + }; + + await esClient.cluster.putComponentTemplate({ + name: componentTemplateName, + // @ts-expect-error - TODO: Remove when ES client typings include support for synthetic source + body, + }); + } + + // Update the installation object to persist the experimental feature map + await updateDatastreamExperimentalFeatures( + soClient, + packagePolicy.package.name, + packagePolicy.package.experimental_data_stream_features + ); + + // Delete the experimental features map from the package policy so it doesn't get persisted + delete packagePolicy.package.experimental_data_stream_features; +} diff --git a/x-pack/plugins/fleet/server/services/package_policies/index.ts b/x-pack/plugins/fleet/server/services/package_policies/index.ts index e79ef475025049..d0d4fa4aae8250 100644 --- a/x-pack/plugins/fleet/server/services/package_policies/index.ts +++ b/x-pack/plugins/fleet/server/services/package_policies/index.ts @@ -5,4 +5,5 @@ * 2.0. */ +export * from './experimental_datastream_features'; export * from './package_policy_name_helper'; diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index 9804f0d1478684..59e4104c624788 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -3502,6 +3502,7 @@ describe('_applyIndexPrivileges()', () => { type: '', dataset: '', title: '', + // @ts-ignore-error release: '', package: '', path: '', diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 3d481db8d63dc0..6cd6f373c1104c 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -9,7 +9,6 @@ import { omit, partition, isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; import semverLt from 'semver/functions/lt'; import { getFlattenedObject } from '@kbn/std'; -import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import type { KibanaRequest, ElasticsearchClient, @@ -24,6 +23,8 @@ import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; import type { AuthenticatedUser } from '@kbn/security-plugin/server'; +import pMap from 'p-map'; + import { packageToPackagePolicy, packageToPackagePolicyInputs, @@ -32,7 +33,12 @@ import { validatePackagePolicy, validationHasErrors, } from '../../common/services'; -import { SO_SEARCH_LIMIT, FLEET_APM_PACKAGE, outputType } from '../../common/constants'; +import { + SO_SEARCH_LIMIT, + FLEET_APM_PACKAGE, + outputType, + PACKAGES_SAVED_OBJECT_TYPE, +} from '../../common/constants'; import type { DeletePackagePoliciesResponse, UpgradePackagePolicyResponse, @@ -46,6 +52,8 @@ import type { UpgradePackagePolicyDryRunResponseItem, RegistryDataStream, PackagePolicyPackage, + Installation, + ExperimentalDataStreamFeature, } from '../../common/types'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../constants'; import { @@ -80,6 +88,8 @@ import { appContextService } from '.'; import { removeOldAssets } from './epm/packages/cleanup'; import type { PackageUpdateEvent, UpdateEventType } from './upgrade_sender'; import { sendTelemetryEvents } from './upgrade_sender'; +import { handleExperimentalDatastreamFeatureOptIn } from './package_policies'; +import { updateDatastreamExperimentalFeatures } from './epm/packages/update'; export type InputsOverride = Partial<NewPackagePolicyInput> & { vars?: Array<NewPackagePolicyInput['vars'] & { name: string }>; @@ -159,6 +169,9 @@ class PackagePolicyService implements PackagePolicyServiceInterface { }); } + // Handle component template/mappings updates for experimental features, e.g. synthetic source + await handleExperimentalDatastreamFeatureOptIn({ soClient, esClient, packagePolicy }); + const pkgInfo = options?.packageInfo ?? (await getPackageInfo({ @@ -216,26 +229,37 @@ class PackagePolicyService implements PackagePolicyServiceInterface { public async bulkCreate( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, - packagePolicies: NewPackagePolicy[], - agentPolicyId: string, - options?: { user?: AuthenticatedUser; bumpRevision?: boolean; force: true } + packagePolicies: NewPackagePolicyWithId[], + options?: { + user?: AuthenticatedUser; + bumpRevision?: boolean; + force?: true; + } ): Promise<PackagePolicy[]> { - await validateIsNotHostedPolicy(soClient, agentPolicyId); + const agentPolicyIds = new Set(packagePolicies.map((pkgPol) => pkgPol.policy_id)); + + for (const agentPolicyId of agentPolicyIds) { + await validateIsNotHostedPolicy(soClient, agentPolicyId, options?.force); + } + const isoDate = new Date().toISOString(); // eslint-disable-next-line @typescript-eslint/naming-convention const { saved_objects } = await soClient.bulkCreate<PackagePolicySOAttributes>( packagePolicies.map((packagePolicy) => { - const packagePolicyId = uuid.v4(); + const packagePolicyId = packagePolicy.id ?? uuid.v4(); + const agentPolicyId = packagePolicy.policy_id; const inputs = packagePolicy.inputs.map((input) => assignStreamIdToInput(packagePolicyId, input) ); + const { id, ...pkgPolicyWithoutId } = packagePolicy; + return { type: SAVED_OBJECT_TYPE, id: packagePolicyId, attributes: { - ...packagePolicy, + ...pkgPolicyWithoutId, inputs, policy_id: agentPolicyId, revision: 1, @@ -254,9 +278,11 @@ class PackagePolicyService implements PackagePolicyServiceInterface { // Assign it to the given agent policy if (options?.bumpRevision ?? true) { - await agentPolicyService.bumpRevision(soClient, esClient, agentPolicyId, { - user: options?.user, - }); + for (const agentPolicyIdT of agentPolicyIds) { + await agentPolicyService.bumpRevision(soClient, esClient, agentPolicyIdT, { + user: options?.user, + }); + } } return newSos.map((newSo) => ({ @@ -279,11 +305,31 @@ class PackagePolicyService implements PackagePolicyServiceInterface { throw new Error(packagePolicySO.error.message); } - return { + let experimentalFeatures: ExperimentalDataStreamFeature[] | undefined; + + if (packagePolicySO.attributes.package?.name) { + const installation = await soClient.get<Installation>( + PACKAGES_SAVED_OBJECT_TYPE, + packagePolicySO.attributes.package?.name + ); + + if (installation && !installation.error) { + experimentalFeatures = installation.attributes?.experimental_data_stream_features; + } + } + + const response = { id: packagePolicySO.id, version: packagePolicySO.version, ...packagePolicySO.attributes, }; + + // If possible, return the experimental features map for the package policy's `package` field + if (experimentalFeatures && response.package) { + response.package.experimental_data_stream_features = experimentalFeatures; + } + + return response; } public async findAllForAgentPolicy( @@ -445,6 +491,9 @@ class PackagePolicyService implements PackagePolicyServiceInterface { elasticsearch = pkgInfo.elasticsearch; } + // Handle component template/mappings updates for experimental features, e.g. synthetic source + await handleExperimentalDatastreamFeatureOptIn({ soClient, esClient, packagePolicy }); + await soClient.update<PackagePolicySOAttributes>( SAVED_OBJECT_TYPE, id, @@ -504,42 +553,54 @@ class PackagePolicyService implements PackagePolicyServiceInterface { ): Promise<DeletePackagePoliciesResponse> { const result: DeletePackagePoliciesResponse = []; - for (const id of ids) { - try { - const packagePolicy = await this.get(soClient, id); - if (!packagePolicy) { - throw new Error('Package policy not found'); - } + const packagePolicies = await this.getByIDs(soClient, ids, { ignoreMissing: true }); + if (!packagePolicies) { + return []; + } - if (packagePolicy.is_managed && !options?.force) { - throw new PackagePolicyRestrictionRelatedError(`Cannot delete package policy ${id}`); - } + const uniqueAgentPolicyIds = [ + ...new Set(packagePolicies.map((packagePolicy) => packagePolicy.policy_id)), + ]; + const hostedAgentPolicies: string[] = []; + + for (const agentPolicyId of uniqueAgentPolicyIds) { + try { await validateIsNotHostedPolicy( soClient, - packagePolicy?.policy_id, + agentPolicyId, options?.force, 'Cannot remove integrations of hosted agent policy' ); + } catch (e) { + hostedAgentPolicies.push(agentPolicyId); + } + } - const agentPolicy = await agentPolicyService - .get(soClient, packagePolicy.policy_id) - .catch((err) => { - if (SavedObjectsErrorHelpers.isNotFoundError(err)) { - appContextService - .getLogger() - .warn(`Agent policy ${packagePolicy.policy_id} not found`); - return null; - } - throw err; - }); + const deletePackagePolicy = async (id: string) => { + try { + const packagePolicy = packagePolicies.find((p) => p.id === id); - await soClient.delete(SAVED_OBJECT_TYPE, id); - if (agentPolicy && !options?.skipUnassignFromAgentPolicies) { - await agentPolicyService.bumpRevision(soClient, esClient, packagePolicy.policy_id, { - user: options?.user, - }); + if (!packagePolicy) { + throw new PackagePolicyNotFoundError( + `Saved object [ingest-package-policies/${id}] not found` + ); + } + + if (packagePolicy.is_managed && !options?.force) { + throw new PackagePolicyRestrictionRelatedError(`Cannot delete package policy ${id}`); + } + + if (hostedAgentPolicies.includes(packagePolicy.policy_id)) { + throw new HostedAgentPolicyRestrictionRelatedError( + 'Cannot remove integrations of hosted agent policy' + ); } + + // TODO: replace this with savedObject BulkDelete when following PR is merged + // https://github.com/elastic/kibana/pull/139680 + await soClient.delete(SAVED_OBJECT_TYPE, id); + result.push({ id, name: packagePolicy.name, @@ -558,6 +619,25 @@ class PackagePolicyService implements PackagePolicyServiceInterface { ...ingestErrorToResponseOptions(error), }); } + }; + + await pMap(ids, deletePackagePolicy, { concurrency: 1000 }); + + if (!options?.skipUnassignFromAgentPolicies) { + const uniquePolicyIdsR = [ + ...new Set(result.filter((r) => r.success && r.policy_id).map((r) => r.policy_id!)), + ]; + + const agentPolicies = await agentPolicyService.getByIDs(soClient, uniquePolicyIdsR); + + for (const policyId of uniquePolicyIdsR) { + const agentPolicy = agentPolicies.find((p) => p.id === policyId); + if (agentPolicy) { + await agentPolicyService.bumpRevision(soClient, esClient, policyId, { + user: options?.user, + }); + } + } } return result; @@ -569,15 +649,23 @@ class PackagePolicyService implements PackagePolicyServiceInterface { id: string, packagePolicy?: PackagePolicy, pkgVersion?: string - ): Promise<{ packagePolicy: PackagePolicy; packageInfo: PackageInfo }> { + ): Promise<{ + packagePolicy: PackagePolicy; + packageInfo: PackageInfo; + experimentalDataStreamFeatures: ExperimentalDataStreamFeature[]; + }> { if (!packagePolicy) { packagePolicy = (await this.get(soClient, id)) ?? undefined; } + + let experimentalDataStreamFeatures: ExperimentalDataStreamFeature[] = []; + if (!pkgVersion && packagePolicy) { const installedPackage = await getInstallation({ savedObjectsClient: soClient, pkgName: packagePolicy.package!.name, }); + if (!installedPackage) { throw new IngestManagerError( i18n.translate('xpack.fleet.packagePolicy.packageNotInstalledError', { @@ -588,9 +676,13 @@ class PackagePolicyService implements PackagePolicyServiceInterface { }) ); } + pkgVersion = installedPackage.version; + experimentalDataStreamFeatures = installedPackage.experimental_data_stream_features ?? []; } + let packageInfo: PackageInfo | undefined; + if (packagePolicy) { packageInfo = await getPackageInfo({ savedObjectsClient: soClient, @@ -601,7 +693,11 @@ class PackagePolicyService implements PackagePolicyServiceInterface { this.validateUpgradePackagePolicy(id, packageInfo, packagePolicy); - return { packagePolicy: packagePolicy!, packageInfo: packageInfo! }; + return { + packagePolicy: packagePolicy!, + packageInfo: packageInfo!, + experimentalDataStreamFeatures, + }; } private validateUpgradePackagePolicy( @@ -659,8 +755,11 @@ class PackagePolicyService implements PackagePolicyServiceInterface { for (const id of ids) { try { - const { packagePolicy: currentPackagePolicy, packageInfo } = - await this.getUpgradePackagePolicyInfo(soClient, id, packagePolicy, pkgVersion); + const { + packagePolicy: currentPackagePolicy, + packageInfo, + experimentalDataStreamFeatures, + } = await this.getUpgradePackagePolicyInfo(soClient, id, packagePolicy, pkgVersion); if (currentPackagePolicy.is_managed && !options?.force) { throw new PackagePolicyRestrictionRelatedError(`Cannot upgrade package policy ${id}`); @@ -673,6 +772,7 @@ class PackagePolicyService implements PackagePolicyServiceInterface { currentPackagePolicy, result, packageInfo, + experimentalDataStreamFeatures, options ); } catch (error) { @@ -694,6 +794,7 @@ class PackagePolicyService implements PackagePolicyServiceInterface { packagePolicy: PackagePolicy, result: UpgradePackagePolicyResponse, packageInfo: PackageInfo, + experimentalDataStreamFeatures: ExperimentalDataStreamFeature[], options?: { user?: AuthenticatedUser } ) { const updatePackagePolicy = updatePackageInputs( @@ -723,6 +824,14 @@ class PackagePolicyService implements PackagePolicyServiceInterface { options, packagePolicy.package!.version ); + + // Persist any experimental feature opt-ins that come through the upgrade process to the Installation SO + await updateDatastreamExperimentalFeatures( + soClient, + packagePolicy.package!.name, + experimentalDataStreamFeatures + ); + result.push({ id, name: packagePolicy.name, @@ -738,12 +847,16 @@ class PackagePolicyService implements PackagePolicyServiceInterface { ): Promise<UpgradePackagePolicyDryRunResponseItem> { try { let packageInfo: PackageInfo; - ({ packagePolicy, packageInfo } = await this.getUpgradePackagePolicyInfo( - soClient, - id, - packagePolicy, - pkgVersion - )); + let experimentalDataStreamFeatures; + + ({ packagePolicy, packageInfo, experimentalDataStreamFeatures } = + await this.getUpgradePackagePolicyInfo(soClient, id, packagePolicy, pkgVersion)); + + // Ensure the experimental features from the Installation saved object come through on the package policy + // during an upgrade dry run + if (packagePolicy.package) { + packagePolicy.package.experimental_data_stream_features = experimentalDataStreamFeatures; + } return this.calculateDiff(soClient, packagePolicy, packageInfo); } catch (error) { @@ -766,6 +879,8 @@ class PackagePolicyService implements PackagePolicyServiceInterface { package: { ...packagePolicy.package!, version: packageInfo.version, + experimental_data_stream_features: + packagePolicy.package?.experimental_data_stream_features, }, }, packageInfo, @@ -873,6 +988,10 @@ class PackagePolicyService implements PackagePolicyServiceInterface { namespace: newPolicy.namespace ?? 'default', description: newPolicy.description ?? '', enabled: newPolicy.enabled ?? true, + package: { + ...newPP.package!, + experimental_data_stream_features: newPolicy.package?.experimental_data_stream_features, + }, policy_id: newPolicy.policy_id ?? agentPolicyId, inputs: newPolicy.inputs[0]?.streams ? newPolicy.inputs : inputs, vars: newPolicy.vars || newPP.vars, @@ -1265,6 +1384,11 @@ function _enforceFrozenVars( return resultVars; } +export interface NewPackagePolicyWithId extends NewPackagePolicy { + id?: string; + policy_id: string; +} + export interface PackagePolicyServiceInterface { create( soClient: SavedObjectsClientContract, @@ -1286,9 +1410,12 @@ export interface PackagePolicyServiceInterface { bulkCreate( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, - packagePolicies: NewPackagePolicy[], - agentPolicyId: string, - options?: { user?: AuthenticatedUser; bumpRevision?: boolean } + packagePolicies: NewPackagePolicyWithId[], + options?: { + user?: AuthenticatedUser; + bumpRevision?: boolean; + force?: true; + } ): Promise<PackagePolicy[]>; get(soClient: SavedObjectsClientContract, id: string): Promise<PackagePolicy | null>; @@ -1379,7 +1506,11 @@ export interface PackagePolicyServiceInterface { getUpgradePackagePolicyInfo( soClient: SavedObjectsClientContract, id: string - ): Promise<{ packagePolicy: PackagePolicy; packageInfo: PackageInfo }>; + ): Promise<{ + packagePolicy: PackagePolicy; + packageInfo: PackageInfo; + experimentalDataStreamFeatures: ExperimentalDataStreamFeature[]; + }>; } export const packagePolicyService: PackagePolicyServiceInterface = new PackagePolicyService(); diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index be923a9fdaa6fc..fd0cee334cc508 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -12,6 +12,7 @@ export type { AgentStatus, AgentType, AgentAction, + ActionStatus, CurrentUpgrade, PackagePolicy, PackagePolicyInput, diff --git a/x-pack/plugins/fleet/server/types/models/package_policy.ts b/x-pack/plugins/fleet/server/types/models/package_policy.ts index 5220f18380dd16..9763d09e205558 100644 --- a/x-pack/plugins/fleet/server/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/package_policy.ts @@ -32,6 +32,9 @@ const PackagePolicyStreamsSchema = { id: schema.maybe(schema.string()), // BWC < 7.11 enabled: schema.boolean(), keep_enabled: schema.maybe(schema.boolean()), + release: schema.maybe( + schema.oneOf([schema.literal('ga'), schema.literal('beta'), schema.literal('experimental')]) + ), data_stream: schema.object({ dataset: schema.string(), type: schema.string(), @@ -87,6 +90,16 @@ const PackagePolicyBaseSchema = { name: schema.string(), title: schema.string(), version: schema.string(), + experimental_data_stream_features: schema.maybe( + schema.arrayOf( + schema.object({ + data_stream: schema.string(), + features: schema.object({ + synthetic_source: schema.boolean(), + }), + }) + ) + ), }) ), // Deprecated TODO create remove issue @@ -111,6 +124,14 @@ const CreatePackagePolicyProps = { name: schema.string(), title: schema.maybe(schema.string()), version: schema.string(), + experimental_data_stream_features: schema.maybe( + schema.arrayOf( + schema.object({ + data_stream: schema.string(), + features: schema.object({ synthetic_source: schema.boolean() }), + }) + ) + ), }) ), // Deprecated TODO create remove issue diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json index d0361f4550576c..7cc16fe6542680 100644 --- a/x-pack/plugins/fleet/tsconfig.json +++ b/x-pack/plugins/fleet/tsconfig.json @@ -6,8 +6,9 @@ "declaration": true, "declarationMap": true, }, + "exclude": ["cypress.config.ts"], "include": [ - // add all the folders containg files to be compiled + // add all the folders containing files to be compiled ".storybook/**/*", "common/**/*", "public/**/*", @@ -15,6 +16,7 @@ "server/**/*.json", "scripts/**/*", "package.json", + "cypress.config.ts", "../../../typings/**/*" ], "references": [ diff --git a/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx b/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx index 77a56eadee526f..3f95b6adf6b451 100644 --- a/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx +++ b/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx @@ -20,8 +20,8 @@ import { i18n } from '@kbn/i18n'; import classNames from 'classnames'; import { FormattedMessage } from '@kbn/i18n-react'; import { connect } from 'react-redux'; -import { IDataPluginServices } from '@kbn/data-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { IUnifiedSearchPluginServices } from '@kbn/unified-search-plugin/public/types'; import { GraphState, hasDatasourceSelector, @@ -75,7 +75,7 @@ function GuidancePanelComponent(props: GuidancePanelProps) { const { onFillWorkspace, onOpenFieldPicker, onIndexPatternSelected, hasDatasource, hasFields } = props; - const kibana = useKibana<IDataPluginServices>(); + const kibana = useKibana<IUnifiedSearchPluginServices>(); const { services, overlays } = kibana; const { savedObjects, uiSettings, application, data } = services; const [hasDataViews, setHasDataViews] = useState<boolean>(true); diff --git a/x-pack/plugins/graph/public/components/search_bar.test.tsx b/x-pack/plugins/graph/public/components/search_bar.test.tsx index 3fbf7222d15297..de9598b52087ea 100644 --- a/x-pack/plugins/graph/public/components/search_bar.test.tsx +++ b/x-pack/plugins/graph/public/components/search_bar.test.tsx @@ -19,8 +19,6 @@ import { import { act } from 'react-dom/test-utils'; import { QueryStringInput } from '@kbn/unified-search-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; -import { setAutocomplete } from '@kbn/unified-search-plugin/public/services'; -import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { I18nProvider, InjectedIntl } from '@kbn/i18n-react'; @@ -106,11 +104,6 @@ describe('search_bar', () => { }, }; - beforeEach(() => { - const autocompleteStart = unifiedSearchPluginMock.createStartContract(); - setAutocomplete(autocompleteStart.autocomplete); - }); - beforeEach(() => { store = createMockGraphStore({ sagas: [submitSearchSaga], diff --git a/x-pack/plugins/graph/public/components/search_bar.tsx b/x-pack/plugins/graph/public/components/search_bar.tsx index 30f3fad82dafc9..9aa05f64754cdd 100644 --- a/x-pack/plugins/graph/public/components/search_bar.tsx +++ b/x-pack/plugins/graph/public/components/search_bar.tsx @@ -12,9 +12,9 @@ import { i18n } from '@kbn/i18n'; import { connect } from 'react-redux'; import { toElasticsearchQuery, fromKueryExpression, Query } from '@kbn/es-query'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { IDataPluginServices } from '@kbn/data-plugin/public'; import { QueryStringInput } from '@kbn/unified-search-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; +import { IUnifiedSearchPluginServices } from '@kbn/unified-search-plugin/public/types'; import { IndexPatternSavedObject, IndexPatternProvider, WorkspaceField } from '../types'; import { openSourceModal } from '../services/source_modal'; import { @@ -95,7 +95,7 @@ export function SearchBarComponent(props: SearchBarStateProps & SearchBarProps) fetchPattern(); }, [currentDatasource, indexPatternProvider, onIndexPatternChange]); - const kibana = useKibana<IDataPluginServices>(); + const kibana = useKibana<IUnifiedSearchPluginServices>(); const { services, overlays } = kibana; const { savedObjects, uiSettings } = services; if (!overlays) return null; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/downsample_interval.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/downsample_interval.test.ts new file mode 100644 index 00000000000000..b24a6d1a32cb7d --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/downsample_interval.test.ts @@ -0,0 +1,130 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; + +import { PhaseWithDownsample } from '../../../../common/types'; +import { setupEnvironment } from '../../helpers'; +import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; + +describe('<EditPolicy /> downsample interval validation', () => { + let testBed: ValidationTestBed; + let actions: ValidationTestBed['actions']; + const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setDefaultResponses(); + httpRequestsMockHelpers.setLoadPolicies([]); + + await act(async () => { + testBed = await setupValidationTestBed(httpSetup); + }); + + const { component } = testBed; + component.update(); + ({ actions } = testBed); + await actions.setPolicyName('mypolicy'); + }); + + [ + { + name: `doesn't allow empty interval`, + value: '', + error: [i18nTexts.editPolicy.errors.numberRequired], + }, + { + name: `doesn't allow 0 for interval`, + value: '0', + error: [i18nTexts.editPolicy.errors.numberGreatThan0Required], + }, + { + name: `doesn't allow -1 for interval`, + value: '-1', + error: [i18nTexts.editPolicy.errors.numberGreatThan0Required], + }, + { + name: `doesn't allow decimals for timing (with dot)`, + value: '5.5', + error: [i18nTexts.editPolicy.errors.integerRequired], + }, + { + name: `doesn't allow decimals for timing (with comma)`, + value: '5,5', + error: [i18nTexts.editPolicy.errors.integerRequired], + }, + ].forEach((testConfig: { name: string; value: string; error: string[] }) => { + (['hot', 'warm', 'cold'] as PhaseWithDownsample[]).forEach((phase: PhaseWithDownsample) => { + const { name, value, error } = testConfig; + test(`${phase}: ${name}`, async () => { + if (phase !== 'hot') { + await actions.togglePhase(phase); + } + + await actions[phase].downsample.toggle(); + + // 1. We first set as dummy value to have a starting min_age value + await actions[phase].downsample.setDownsampleInterval('111'); + // 2. At this point we are sure there will be a change of value and that any validation + // will be displayed under the field. + await actions[phase].downsample.setDownsampleInterval(value); + + actions.errors.waitForValidation(); + + actions.errors.expectMessages(error); + }); + }); + }); + + test('should validate an interval is greater or multiple than previous phase interval', async () => { + await actions.togglePhase('warm'); + await actions.togglePhase('cold'); + + await actions.hot.downsample.toggle(); + await actions.hot.downsample.setDownsampleInterval('60', 'm'); + + await actions.warm.downsample.toggle(); + await actions.warm.downsample.setDownsampleInterval('1', 'h'); + + actions.errors.waitForValidation(); + actions.errors.expectMessages( + ['Must be greater than and a multiple of the hot phase value (60m)'], + 'warm' + ); + + await actions.cold.downsample.toggle(); + await actions.cold.downsample.setDownsampleInterval('90', 'm'); + actions.errors.waitForValidation(); + actions.errors.expectMessages( + ['Must be greater than and a multiple of the warm phase value (1h)'], + 'cold' + ); + + // disable warm phase; + await actions.togglePhase('warm'); + // TODO: there is a bug that disabling a phase doesn't trigger downsample validation in other phases, + // users can work around it by changing the value + await actions.cold.downsample.setDownsampleInterval('120', 'm'); + actions.errors.waitForValidation(); + actions.errors.expectMessages([], 'cold'); + + await actions.cold.downsample.setDownsampleInterval('90', 'm'); + actions.errors.waitForValidation(); + actions.errors.expectMessages( + ['Must be greater than and a multiple of the hot phase value (60m)'], + 'cold' + ); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts index 14f1403ad536d7..321a7efbfc5e3c 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts @@ -201,6 +201,8 @@ describe('<EditPolicy /> serialization', () => { await actions.hot.setShrinkCount('2'); await actions.hot.toggleReadonly(); await actions.hot.setIndexPriority('123'); + await actions.hot.downsample.toggle(); + await actions.hot.downsample.setDownsampleInterval('2', 'h'); await actions.savePolicy(); @@ -231,6 +233,7 @@ describe('<EditPolicy /> serialization', () => { priority: 123, }, readonly: {}, + downsample: { fixed_interval: '2h' }, }, }, }, @@ -323,6 +326,8 @@ describe('<EditPolicy /> serialization', () => { await actions.warm.setBestCompression(true); await actions.warm.toggleReadonly(); await actions.warm.setIndexPriority('123'); + await actions.warm.downsample.toggle(); + await actions.warm.downsample.setDownsampleInterval('20', 'm'); await actions.savePolicy(); expect(httpSetup.post).toHaveBeenLastCalledWith( @@ -360,6 +365,7 @@ describe('<EditPolicy /> serialization', () => { number_of_replicas: 123, }, readonly: {}, + downsample: { fixed_interval: '20m' }, }, }, }, @@ -463,6 +469,8 @@ describe('<EditPolicy /> serialization', () => { await actions.cold.setReplicas('123'); await actions.cold.toggleReadonly(); await actions.cold.setIndexPriority('123'); + await actions.cold.downsample.toggle(); + await actions.cold.downsample.setDownsampleInterval('5'); await actions.savePolicy(); @@ -494,6 +502,7 @@ describe('<EditPolicy /> serialization', () => { number_of_replicas: 123, }, readonly: {}, + downsample: { fixed_interval: '5d' }, }, }, }, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/downsample_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/downsample_actions.ts new file mode 100644 index 00000000000000..315ed3d58520a6 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/downsample_actions.ts @@ -0,0 +1,44 @@ +/* + * 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 { TestBed } from '@kbn/test-jest-helpers'; +import { act } from 'react-dom/test-utils'; +import { Phase } from '../../../../common/types'; +import { createFormToggleAction } from '..'; + +const createSetDownsampleIntervalAction = + (testBed: TestBed, phase: Phase) => async (value: string, units?: string) => { + const { find, component } = testBed; + + await act(async () => { + find(`${phase}-downsampleFixedInterval`).simulate('change', { target: { value } }); + }); + component.update(); + + if (units) { + act(() => { + find(`${phase}-downsampleFixedIntervalUnits.show-filters-button`).simulate('click'); + }); + component.update(); + + act(() => { + find(`${phase}-downsampleFixedIntervalUnits.filter-option-${units}`).simulate('click'); + }); + component.update(); + } + }; + +export const createDownsampleActions = (testBed: TestBed, phase: Phase) => { + const { exists } = testBed; + return { + downsample: { + exists: () => exists(`${phase}-downsampleSwitch`), + toggle: createFormToggleAction(testBed, `${phase}-downsampleSwitch`), + setDownsampleInterval: createSetDownsampleIntervalAction(testBed, phase), + }, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index.ts index f2579031dbad98..fefbd0363d4499 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index.ts @@ -21,6 +21,7 @@ export { createForceMergeActions } from './forcemerge_actions'; export { createReadonlyActions } from './readonly_actions'; export { createIndexPriorityActions } from './index_priority_actions'; export { createShrinkActions } from './shrink_actions'; +export { createDownsampleActions } from './downsample_actions'; export { createHotPhaseActions, createWarmPhaseActions, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/phases.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/phases.ts index d317b09d4d3056..ffe3aad05b0ebd 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/phases.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/phases.ts @@ -16,6 +16,7 @@ import { createNodeAllocationActions, createReplicasAction, createSnapshotPolicyActions, + createDownsampleActions, } from '.'; export const createHotPhaseActions = (testBed: TestBed) => { @@ -26,6 +27,7 @@ export const createHotPhaseActions = (testBed: TestBed) => { ...createReadonlyActions(testBed, 'hot'), ...createIndexPriorityActions(testBed, 'hot'), ...createSearchableSnapshotActions(testBed, 'hot'), + ...createDownsampleActions(testBed, 'hot'), }, }; }; @@ -39,6 +41,7 @@ export const createWarmPhaseActions = (testBed: TestBed) => { ...createIndexPriorityActions(testBed, 'warm'), ...createNodeAllocationActions(testBed, 'warm'), ...createReplicasAction(testBed, 'warm'), + ...createDownsampleActions(testBed, 'warm'), }, }; }; @@ -51,6 +54,7 @@ export const createColdPhaseActions = (testBed: TestBed) => { ...createIndexPriorityActions(testBed, 'cold'), ...createNodeAllocationActions(testBed, 'cold'), ...createSearchableSnapshotActions(testBed, 'cold'), + ...createDownsampleActions(testBed, 'cold'), }, }; }; diff --git a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts index 6c5a9b7c36c508..fcc9e89da47963 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -15,6 +15,8 @@ export type PhaseWithTiming = keyof Omit<Phases, 'hot'>; export type PhaseExceptDelete = keyof Omit<Phases, 'delete'>; +export type PhaseWithDownsample = 'hot' | 'warm' | 'cold'; + export interface SerializedPolicy { name: string; phases: Phases; @@ -93,6 +95,7 @@ export interface SerializedHotPhase extends SerializedPhase { forcemerge?: ForcemergeAction; readonly?: {}; shrink?: ShrinkAction; + downsample?: DownsampleAction; set_priority?: { priority: number | null; @@ -110,6 +113,7 @@ export interface SerializedWarmPhase extends SerializedPhase { shrink?: ShrinkAction; forcemerge?: ForcemergeAction; readonly?: {}; + downsample?: DownsampleAction; set_priority?: { priority: number | null; }; @@ -121,6 +125,7 @@ export interface SerializedColdPhase extends SerializedPhase { actions: { freeze?: {}; readonly?: {}; + downsample?: DownsampleAction; allocate?: AllocateAction; set_priority?: { priority: number | null; @@ -178,6 +183,10 @@ export interface ForcemergeAction { index_codec?: 'best_compression'; } +export interface DownsampleAction { + fixed_interval: string; +} + export interface LegacyPolicy { name: string; phases: { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx index 58f85441740441..e3214eab3ec47d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx @@ -15,6 +15,7 @@ import { IndexPriorityField, ReplicasField, ReadonlyField, + DownsampleField, } from '../shared_fields'; import { Phase } from '../phase'; @@ -38,6 +39,8 @@ export const ColdPhase: FunctionComponent = () => { {/* Readonly section */} {!isUsingSearchableSnapshotInHotPhase && <ReadonlyField phase="cold" />} + {!isUsingSearchableSnapshotInHotPhase && <DownsampleField phase="cold" />} + {/* Data tier allocation section */} <DataTierAllocationField description={i18nTexts.dataTierAllocation.description} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx index c5d1cc921dda63..4d645a9d2066b6 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx @@ -29,6 +29,7 @@ import { SearchableSnapshotField, ReadonlyField, ShrinkField, + DownsampleField, } from '../shared_fields'; import { Phase } from '../phase'; @@ -176,6 +177,7 @@ export const HotPhase: FunctionComponent = () => { <ShrinkField phase={'hot'} /> {license.canUseSearchableSnapshot() && <SearchableSnapshotField phase="hot" />} <ReadonlyField phase={'hot'} /> + <DownsampleField phase="hot" /> </> )} <IndexPriorityField phase={'hot'} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/downsample_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/downsample_field.tsx new file mode 100644 index 00000000000000..e25068be8b13a5 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/downsample_field.tsx @@ -0,0 +1,82 @@ +/* + * 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 from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiTextColor } from '@elastic/eui'; +import { UnitField } from './unit_field'; +import { fixedIntervalUnits } from '../../../constants'; +import { UseField } from '../../../form'; +import { NumericField } from '../../../../../../shared_imports'; +import { ToggleFieldWithDescribedFormRow } from '../../described_form_row'; +// import { LearnMoreLink } from '../../learn_more_link'; +import { i18nTexts } from '../../../i18n_texts'; +import { PhaseWithDownsample } from '../../../../../../../common/types'; + +interface Props { + phase: PhaseWithDownsample; +} + +export const DownsampleField: React.FunctionComponent<Props> = ({ phase }) => { + // const { docLinks } = useKibana().services; + + const downsampleEnabledPath = `_meta.${phase}.downsample.enabled`; + const downsampleIntervalSizePath = `_meta.${phase}.downsample.fixedIntervalSize`; + const downsampleIntervalUnitsPath = `_meta.${phase}.downsample.fixedIntervalUnits`; + + return ( + <ToggleFieldWithDescribedFormRow + title={ + <h3> + <FormattedMessage + id="xpack.indexLifecycleMgmt.editPolicy.downsampleTitle" + defaultMessage="Downsample" + /> + </h3> + } + description={ + <EuiTextColor color="subdued"> + <FormattedMessage + id="xpack.indexLifecycleMgmt.editPolicy.downsampleDescription" + defaultMessage="Roll up documents within a fixed interval to a single summary document. Reduces the index footprint by storing time series data at reduced granularity." + />{' '} + {/* TODO: add when available*/} + {/* <LearnMoreLink docPath={docLinks.links.elasticsearch.ilmDownsample} /> */} + </EuiTextColor> + } + fullWidth + titleSize="xs" + switchProps={{ + 'data-test-subj': `${phase}-downsampleSwitch`, + path: downsampleEnabledPath, + }} + > + <UseField + path={downsampleIntervalSizePath} + key={downsampleIntervalSizePath} + component={NumericField} + componentProps={{ + fullWidth: false, + euiFieldProps: { + 'data-test-subj': `${phase}-downsampleFixedInterval`, + min: 1, + append: ( + <UnitField + path={downsampleIntervalUnitsPath} + options={fixedIntervalUnits} + euiFieldProps={{ + 'data-test-subj': `${phase}-downsampleFixedIntervalUnits`, + 'aria-label': i18nTexts.editPolicy.downsampleIntervalFieldUnitsLabel, + }} + /> + ), + }, + }} + /> + </ToggleFieldWithDescribedFormRow> + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts index 220f0bd8e941a8..34f4d09877e3ab 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts @@ -22,3 +22,5 @@ export { ReadonlyField } from './readonly_field'; export { ReplicasField } from './replicas_field'; export { IndexPriorityField } from './index_priority_field'; + +export { DownsampleField } from './downsample_field'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx index fbf5dc5c5af4b3..f36067923e1b7e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx @@ -265,7 +265,7 @@ export const SearchableSnapshotField: FunctionComponent<Props> = ({ 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotCalloutBody', { defaultMessage: - 'Force merge, shrink and read only actions are not allowed when converting data to a fully-mounted index in this phase.', + 'Force merge, shrink, downsample and read only actions are not allowed when converting data to a fully-mounted index in this phase.', } )} data-test-subj="searchableSnapshotFieldsDisabledCallout" diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx index 29445ac8e4715e..fba9556b5f4eaa 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx @@ -17,6 +17,7 @@ import { ShrinkField, ReadonlyField, ReplicasField, + DownsampleField, } from '../shared_fields'; import { Phase } from '../phase'; @@ -42,6 +43,8 @@ export const WarmPhase: FunctionComponent = () => { {!isUsingSearchableSnapshotInHotPhase && <ReadonlyField phase="warm" />} + {!isUsingSearchableSnapshotInHotPhase && <DownsampleField phase="warm" />} + {/* Data tier allocation section */} <DataTierAllocationField description={i18nTexts.dataTierAllocation.description} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts index 9b708e0d464d22..f36c7cc69977b8 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts @@ -118,3 +118,42 @@ export const timeUnits = [ }), }, ]; + +/* + * Labels for fixed intervals + */ +export const fixedIntervalUnits = [ + { + value: 'd', + text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.fixedIntervalUnits.daysLabel', { + defaultMessage: 'days', + }), + }, + { + value: 'h', + text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.fixedIntervalUnits.hoursLabel', { + defaultMessage: 'hours', + }), + }, + { + value: 'm', + text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.fixedIntervalUnits.minutesLabel', { + defaultMessage: 'minutes', + }), + }, + { + value: 's', + text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.fixedIntervalUnits.secondsLabel', { + defaultMessage: 'seconds', + }), + }, + { + value: 'ms', + text: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.fixedIntervalUnits.millisecondsLabel', + { + defaultMessage: 'milliseconds', + } + ), + }, +]; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts index 73c15c864b2af7..6a0dae8d3deaf7 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts @@ -40,6 +40,9 @@ export const createDeserializer = bestCompression: hot?.actions?.forcemerge?.index_codec === 'best_compression', readonlyEnabled: Boolean(hot?.actions?.readonly), shrink: { isUsingShardSize: Boolean(hot?.actions.shrink?.max_primary_shard_size) }, + downsample: { + enabled: Boolean(hot?.actions?.downsample), + }, }, warm: { enabled: Boolean(warm), @@ -49,12 +52,18 @@ export const createDeserializer = readonlyEnabled: Boolean(warm?.actions?.readonly), minAgeToMilliSeconds: -1, shrink: { isUsingShardSize: Boolean(warm?.actions.shrink?.max_primary_shard_size) }, + downsample: { + enabled: Boolean(warm?.actions?.downsample), + }, }, cold: { enabled: Boolean(cold), dataTierAllocationType: determineDataTierAllocationType(cold?.actions), readonlyEnabled: Boolean(cold?.actions?.readonly), minAgeToMilliSeconds: -1, + downsample: { + enabled: Boolean(cold?.actions?.downsample), + }, }, frozen: { enabled: Boolean(frozen), @@ -105,6 +114,14 @@ export const createDeserializer = draft._meta.hot.shrink.maxPrimaryShardSizeUnits = primaryShardSize.units; } + if (draft.phases.hot?.actions.downsample?.fixed_interval) { + const downsampleInterval = splitSizeAndUnits( + draft.phases.hot.actions.downsample.fixed_interval + ); + draft._meta.hot.downsample.fixedIntervalUnits = downsampleInterval.units; + draft._meta.hot.downsample.fixedIntervalSize = downsampleInterval.size; + } + if (draft.phases.warm) { if (draft.phases.warm.actions?.allocate?.require) { Object.entries(draft.phases.warm.actions.allocate.require).forEach((entry) => { @@ -125,6 +142,14 @@ export const createDeserializer = draft.phases.warm.actions.shrink.max_primary_shard_size = primaryShardSize.size; draft._meta.warm.shrink.maxPrimaryShardSizeUnits = primaryShardSize.units; } + + if (draft.phases.warm?.actions.downsample?.fixed_interval) { + const downsampleInterval = splitSizeAndUnits( + draft.phases.warm.actions.downsample.fixed_interval + ); + draft._meta.warm.downsample.fixedIntervalUnits = downsampleInterval.units; + draft._meta.warm.downsample.fixedIntervalSize = downsampleInterval.size; + } } if (draft.phases.cold) { @@ -139,6 +164,14 @@ export const createDeserializer = draft.phases.cold.min_age = minAge.size; draft._meta.cold.minAgeUnit = minAge.units; } + + if (draft.phases.cold?.actions.downsample?.fixed_interval) { + const downsampleInterval = splitSizeAndUnits( + draft.phases.cold.actions.downsample.fixed_interval + ); + draft._meta.cold.downsample.fixedIntervalUnits = downsampleInterval.units; + draft._meta.cold.downsample.fixedIntervalSize = downsampleInterval.size; + } } if (draft.phases.frozen) { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index 0e9f4bd953c2ab..2299cd2fed3444 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -7,17 +7,22 @@ import { i18n } from '@kbn/i18n'; -import { PhaseExceptDelete, PhaseWithTiming } from '../../../../../common/types'; -import { FormSchema, fieldValidators } from '../../../../shared_imports'; +import { + PhaseExceptDelete, + PhaseWithDownsample, + PhaseWithTiming, +} from '../../../../../common/types'; +import { fieldValidators, FormSchema } from '../../../../shared_imports'; import { defaultIndexPriority } from '../../../constants'; -import { ROLLOVER_FORM_PATHS, CLOUD_DEFAULT_REPO } from '../constants'; +import { CLOUD_DEFAULT_REPO, ROLLOVER_FORM_PATHS } from '../constants'; import { i18nTexts } from '../i18n_texts'; import { ifExistsNumberGreaterThanZero, ifExistsNumberNonNegative, - rolloverThresholdsValidator, integerValidator, minAgeGreaterThanPreviousPhase, + rolloverThresholdsValidator, + downsampleIntervalMultipleOfPreviousOne, } from './validations'; const rolloverFormPaths = Object.values(ROLLOVER_FORM_PATHS); @@ -156,6 +161,54 @@ const getMinAgeField = (phase: PhaseWithTiming, defaultValue?: string) => ({ ], }); +const getDownsampleFieldsToValidateOnChange = ( + p: PhaseWithDownsample, + includeCurrentPhase = true +) => { + const allPhases: PhaseWithDownsample[] = ['hot', 'warm', 'cold']; + const getIntervalSizePath = (currentPhase: PhaseWithDownsample) => + `_meta.${currentPhase}.downsample.fixedIntervalSize`; + const omitPreviousPhases = (currentPhase: PhaseWithDownsample) => + allPhases.slice(allPhases.indexOf(currentPhase) + (includeCurrentPhase ? 0 : 1)); + // when a phase is validated, need to also validate all downsample intervals in the next phases + return omitPreviousPhases(p).map(getIntervalSizePath); +}; +const getDownsampleSchema = (phase: PhaseWithDownsample): FormSchema['downsample'] => { + return { + enabled: { + defaultValue: false, + label: i18nTexts.editPolicy.downsampleEnabledFieldLabel, + fieldsToValidateOnChange: getDownsampleFieldsToValidateOnChange( + phase, + /* don't trigger validation on the current validation to prevent showing error state on pristine input */ + false + ), + }, + fixedIntervalSize: { + label: i18nTexts.editPolicy.downsampleIntervalFieldLabel, + fieldsToValidateOnChange: getDownsampleFieldsToValidateOnChange(phase), + validations: [ + { + validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), + }, + { + validator: ifExistsNumberGreaterThanZero, + }, + { + validator: integerValidator, + }, + { + validator: downsampleIntervalMultipleOfPreviousOne(phase), + }, + ], + }, + fixedIntervalUnits: { + defaultValue: 'd', + fieldsToValidateOnChange: getDownsampleFieldsToValidateOnChange(phase), + }, + }; +}; + export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ _meta: { hot: { @@ -197,6 +250,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ defaultValue: 'gb', }, }, + downsample: getDownsampleSchema('hot'), }, warm: { enabled: { @@ -239,6 +293,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ defaultValue: 'gb', }, }, + downsample: getDownsampleSchema('warm'), }, cold: { enabled: { @@ -269,6 +324,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ allocationNodeAttribute: { label: i18nTexts.editPolicy.allocationNodeAttributeFieldLabel, }, + downsample: getDownsampleSchema('cold'), }, frozen: { enabled: { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts index 75fb48a5becd3c..c0ae46bf18c4fd 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -124,6 +124,19 @@ export const createSerializer = } else { delete hotPhaseActions.shrink!.max_primary_shard_size; } + + /** + * HOT PHASE DOWNSAMPLE + */ + if (_meta.hot?.downsample?.enabled) { + hotPhaseActions.downsample = { + ...hotPhaseActions.downsample, + fixed_interval: `${_meta.hot.downsample.fixedIntervalSize!}${_meta.hot.downsample + .fixedIntervalUnits!}`, + }; + } else { + delete hotPhaseActions.downsample; + } } else { delete hotPhaseActions.rollover; delete hotPhaseActions.forcemerge; @@ -214,6 +227,19 @@ export const createSerializer = } else { delete warmPhase.actions.shrink!.max_primary_shard_size; } + + /** + * WARM PHASE DOWNSAMPLE + */ + if (_meta.warm?.downsample?.enabled) { + warmPhase.actions.downsample = { + ...warmPhase.actions.downsample, + fixed_interval: `${_meta.warm.downsample.fixedIntervalSize!}${_meta.warm.downsample + .fixedIntervalUnits!}`, + }; + } else { + delete warmPhase.actions.downsample; + } } else { delete draft.phases.warm; } @@ -278,6 +304,19 @@ export const createSerializer = } else { delete coldPhase.actions.searchable_snapshot; } + + /** + * COLD PHASE DOWNSAMPLE + */ + if (_meta.cold?.downsample?.enabled) { + coldPhase.actions.downsample = { + ...coldPhase.actions.downsample, + fixed_interval: `${_meta.cold.downsample.fixedIntervalSize!}${_meta.cold.downsample + .fixedIntervalUnits!}`, + }; + } else { + delete coldPhase.actions.downsample; + } } else { delete draft.phases.cold; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts index 04f59707ea634d..5035071a1f2a14 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import moment from 'moment'; import { fieldValidators, ValidationFunc, @@ -16,7 +17,7 @@ import { import { ROLLOVER_FORM_PATHS } from '../constants'; import { i18nTexts } from '../i18n_texts'; -import { PhaseWithTiming, PolicyFromES } from '../../../../../common/types'; +import { PhaseWithDownsample, PhaseWithTiming, PolicyFromES } from '../../../../../common/types'; import { FormInternal } from '../types'; const { numberGreaterThanField, containsCharsField, emptyField, startsWithField } = fieldValidators; @@ -272,3 +273,103 @@ export const minAgeGreaterThanPreviousPhase = } } }; + +export const downsampleIntervalMultipleOfPreviousOne = + (phase: PhaseWithDownsample) => + ({ formData }: { formData: Record<string, any> }) => { + if (phase === 'hot') return; + + const getValueFor = (_phase: PhaseWithDownsample) => { + const intervalSize = formData[`_meta.${_phase}.downsample.fixedIntervalSize`]; + const intervalUnits = formData[`_meta.${_phase}.downsample.fixedIntervalUnits`]; + + if (!intervalSize || !intervalUnits) { + return null; + } + + const milliseconds = moment.duration(intervalSize, intervalUnits).asMilliseconds(); + const esFormat = intervalSize + intervalUnits; + + return { + milliseconds, + esFormat, + }; + }; + + const intervalValues = { + hot: getValueFor('hot'), + warm: getValueFor('warm'), + cold: getValueFor('cold'), + }; + + const checkIfGreaterAndMultiple = (nextInterval: number, previousInterval: number): boolean => + nextInterval > previousInterval && nextInterval % previousInterval === 0; + + if (phase === 'warm' && intervalValues.warm) { + if (intervalValues.hot) { + if ( + !checkIfGreaterAndMultiple( + intervalValues.warm.milliseconds, + intervalValues.hot.milliseconds + ) + ) { + return { + message: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.downsamplePreviousIntervalWarmPhaseError', + { + defaultMessage: + 'Must be greater than and a multiple of the hot phase value ({value})', + values: { + value: intervalValues.hot.esFormat, + }, + } + ), + }; + } + } + } + + if (phase === 'cold' && intervalValues.cold) { + if (intervalValues.warm) { + if ( + !checkIfGreaterAndMultiple( + intervalValues.cold.milliseconds, + intervalValues.warm.milliseconds + ) + ) { + return { + message: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.downsamplePreviousIntervalColdPhaseWarmError', + { + defaultMessage: + 'Must be greater than and a multiple of the warm phase value ({value})', + values: { + value: intervalValues.warm.esFormat, + }, + } + ), + }; + } + } else if (intervalValues.hot) { + if ( + !checkIfGreaterAndMultiple( + intervalValues.cold.milliseconds, + intervalValues.hot.milliseconds + ) + ) { + return { + message: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.downsamplePreviousIntervalColdPhaseHotError', + { + defaultMessage: + 'Must be greater than and a multiple of the hot phase value ({value})', + values: { + value: intervalValues.hot.esFormat, + }, + } + ), + }; + } + } + } + }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts index f54a782ea13345..b4d9c5282d6782 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts @@ -54,6 +54,21 @@ export const i18nTexts = { readonlyEnabledFieldLabel: i18n.translate('xpack.indexLifecycleMgmt.readonlyFieldLabel', { defaultMessage: 'Make index read only', }), + downsampleEnabledFieldLabel: i18n.translate('xpack.indexLifecycleMgmt.downsampleFieldLabel', { + defaultMessage: 'Enable downsampling', + }), + downsampleIntervalFieldLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.downsampleIntervalFieldLabel', + { + defaultMessage: 'Downsampling interval', + } + ), + downsampleIntervalFieldUnitsLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.downsampleIntervalFieldUnitsLabel', + { + defaultMessage: 'Downsampling interval units', + } + ), maxNumSegmentsFieldLabel: i18n.translate( 'xpack.indexLifecycleMgmt.forceMerge.numberOfSegmentsLabel', { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts index 8e83f123a8fa22..5dd5477cae2c25 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts @@ -29,7 +29,15 @@ interface ShrinkFields { }; } -interface HotPhaseMetaFields extends ForcemergeFields, ShrinkFields { +export interface DownsampleFields { + downsample: { + enabled: boolean; + fixedIntervalSize?: string; + fixedIntervalUnits?: string; + }; +} + +interface HotPhaseMetaFields extends ForcemergeFields, ShrinkFields, DownsampleFields { /** * By default rollover is enabled with set values for max age, max size and max docs. In this policy form * opting in to default rollover overrides custom rollover values. @@ -58,13 +66,14 @@ interface WarmPhaseMetaFields extends DataAllocationMetaFields, MinAgeField, ForcemergeFields, - ShrinkFields { + ShrinkFields, + DownsampleFields { enabled: boolean; warmPhaseOnRollover: boolean; readonlyEnabled: boolean; } -interface ColdPhaseMetaFields extends DataAllocationMetaFields, MinAgeField { +interface ColdPhaseMetaFields extends DataAllocationMetaFields, MinAgeField, DownsampleFields { enabled: boolean; readonlyEnabled: boolean; } diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx index ae5915a032b8e5..c00885de6b9678 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx @@ -1078,4 +1078,18 @@ export const PARAMETERS_DEFINITION: { [key in ParameterName]: ParameterDefinitio }, schema: t.union([t.literal(2), t.literal(3), t.literal(4)]), }, + time_series_metric: { + fieldConfig: { + defaultValue: null, + type: FIELD_TYPES.SELECT, + }, + schema: t.union([t.literal('gauge'), t.literal('counter'), t.null]), + }, + time_series_dimension: { + fieldConfig: { + type: FIELD_TYPES.CHECKBOX, + defaultValue: false, + }, + schema: t.boolean, + }, }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts index 408339f5a10a69..6e50cdfe3ba9ad 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts @@ -156,7 +156,9 @@ export type ParameterName = | 'relations' | 'max_shingle_size' | 'value' - | 'meta'; + | 'meta' + | 'time_series_metric' + | 'time_series_dimension'; export interface Parameter { fieldConfig: FieldConfig; diff --git a/x-pack/plugins/lens/common/expressions/collapse/collapse_fn.test.ts b/x-pack/plugins/lens/common/expressions/collapse/collapse_fn.test.ts index f8786534309547..ed9f46f96b44c6 100644 --- a/x-pack/plugins/lens/common/expressions/collapse/collapse_fn.test.ts +++ b/x-pack/plugins/lens/common/expressions/collapse/collapse_fn.test.ts @@ -33,12 +33,45 @@ describe('collapse_fn', () => { { val: 8, split: 'B' }, ], }, - { metric: ['val'], fn: 'sum' } + { metric: ['val'], fn: ['sum'] } ); expect(result.rows).toEqual([{ val: 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 }]); }); + it('can use a single function for multiple metrics', async () => { + const result = await runFn( + { + type: 'datatable', + columns: [ + { id: 'val', name: 'val', meta: { type: 'number' } }, + { id: 'val2', name: 'val2', meta: { type: 'number' } }, + { id: 'val3', name: 'val3', meta: { type: 'number' } }, + { id: 'split', name: 'split', meta: { type: 'string' } }, + ], + rows: [ + { val: 1, val2: 1, val3: 1, split: 'A' }, + { val: 2, val2: 2, val3: 2, split: 'B' }, + { val: 3, val2: 3, val3: 3, split: 'B' }, + { val: 4, val2: 4, val3: 4, split: 'A' }, + { val: 5, val2: 5, val3: 5, split: 'A' }, + { val: 6, val2: 6, val3: 6, split: 'A' }, + { val: 7, val2: 7, val3: 7, split: 'B' }, + { val: 8, val2: 22, val3: 77, split: 'B' }, + ], + }, + { metric: ['val', 'val2', 'val3'], fn: ['sum'] } + ); + + expect(result.rows).toEqual([ + { + val: 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8, + val2: 1 + 2 + 3 + 4 + 5 + 6 + 7 + 22, + val3: 1 + 2 + 3 + 4 + 5 + 6 + 7 + 77, + }, + ]); + }); + it('can use different functions for each different metric', async () => { const result = await runFn( { @@ -114,7 +147,7 @@ describe('collapse_fn', () => { }; it('splits by a column', async () => { - const result = await runFn(twoSplitTable, { metric: ['val'], by: ['split'], fn: 'sum' }); + const result = await runFn(twoSplitTable, { metric: ['val'], by: ['split'], fn: ['sum'] }); expect(result.rows).toEqual([ { val: 1 + 4 + 6, split: 'A' }, { val: 2 + 7 + 8, split: 'B' }, @@ -123,7 +156,7 @@ describe('collapse_fn', () => { }); it('applies avg', async () => { - const result = await runFn(twoSplitTable, { metric: ['val'], by: ['split'], fn: 'avg' }); + const result = await runFn(twoSplitTable, { metric: ['val'], by: ['split'], fn: ['avg'] }); expect(result.rows).toEqual([ { val: (1 + 4 + 6) / 3, split: 'A' }, { val: (2 + 7 + 8) / 3, split: 'B' }, @@ -132,7 +165,7 @@ describe('collapse_fn', () => { }); it('applies min', async () => { - const result = await runFn(twoSplitTable, { metric: ['val'], by: ['split'], fn: 'min' }); + const result = await runFn(twoSplitTable, { metric: ['val'], by: ['split'], fn: ['min'] }); expect(result.rows).toEqual([ { val: 1, split: 'A' }, { val: 2, split: 'B' }, @@ -141,7 +174,7 @@ describe('collapse_fn', () => { }); it('applies max', async () => { - const result = await runFn(twoSplitTable, { metric: ['val'], by: ['split'], fn: 'max' }); + const result = await runFn(twoSplitTable, { metric: ['val'], by: ['split'], fn: ['max'] }); expect(result.rows).toEqual([ { val: 6, split: 'A' }, { val: 8, split: 'B' }, diff --git a/x-pack/plugins/lens/common/expressions/collapse/collapse_fn.ts b/x-pack/plugins/lens/common/expressions/collapse/collapse_fn.ts index 5ca2248ed1ef7a..ee3192705332d6 100644 --- a/x-pack/plugins/lens/common/expressions/collapse/collapse_fn.ts +++ b/x-pack/plugins/lens/common/expressions/collapse/collapse_fn.ts @@ -17,11 +17,8 @@ function getValueAsNumberArray(value: unknown) { } export const collapseFn: CollapseExpressionFunction['fn'] = (input, { by, metric, fn }) => { - const collapseFunctionsByMetricIndex = Array.isArray(fn) - ? fn - : metric - ? new Array(metric.length).fill(fn) - : []; + const collapseFunctionsByMetricIndex = + fn.length > 1 ? fn : metric ? new Array(metric.length).fill(fn[0]) : []; if (metric && metric.length !== collapseFunctionsByMetricIndex.length) { throw Error(`lens_collapse - Called with ${metric.length} metrics and ${fn.length} collapse functions. diff --git a/x-pack/plugins/lens/common/expressions/collapse/index.ts b/x-pack/plugins/lens/common/expressions/collapse/index.ts index 5ea792e39cb0d9..bd8df507c95e8a 100644 --- a/x-pack/plugins/lens/common/expressions/collapse/index.ts +++ b/x-pack/plugins/lens/common/expressions/collapse/index.ts @@ -13,7 +13,7 @@ type CollapseFunction = 'sum' | 'avg' | 'min' | 'max'; export interface CollapseArgs { by?: string[]; metric?: string[]; - fn: CollapseFunction | CollapseFunction[]; + fn: CollapseFunction[]; } /** diff --git a/x-pack/plugins/lens/common/types.ts b/x-pack/plugins/lens/common/types.ts index f4ad9e4fb4a760..bb176babf9d2f9 100644 --- a/x-pack/plugins/lens/common/types.ts +++ b/x-pack/plugins/lens/common/types.ts @@ -66,6 +66,7 @@ export interface SharedPieLayerState { primaryGroups: string[]; secondaryGroups?: string[]; metric?: string; + collapseFns?: Record<string, string>; numberDisplay: NumberDisplayType; categoryDisplay: CategoryDisplayType; legendDisplay: LegendDisplayType; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index 0440700c7ed8b6..cb45519250a81d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -249,6 +249,52 @@ describe('LayerPanel', () => { expect(group).toHaveLength(1); }); + it('should tell the user to remove the correct number of dimensions', async () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [{ columnId: 'x' }], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + dimensionsTooMany: 1, + }, + { + groupLabel: 'A', + groupId: 'a', + accessors: [{ columnId: 'x' }], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + dimensionsTooMany: -1, + }, + { + groupLabel: 'B', + groupId: 'b', + accessors: [], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + dimensionsTooMany: 3, + }, + ], + }); + + const { instance } = await mountWithProvider(<LayerPanel {...getDefaultProps()} />); + + const groups = instance.find(EuiFormRow); + + expect(groups.findWhere((e) => e.prop('error') === 'Please remove a dimension')).toHaveLength( + 1 + ); + expect( + groups.findWhere((e) => e.prop('error') === 'Please remove 3 dimensions') + ).toHaveLength(1); + expect(groups.findWhere((e) => e.prop('error') === '')).toHaveLength(1); + }); + it('should render the required warning when only one group is configured (with requiredMinDimensionCount)', async () => { mockVisualization.getConfiguration.mockReturnValue({ groups: [ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 65008196c4b8fe..d1ab12342dd83d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -370,26 +370,36 @@ export function LayerPanel( </header> {groups.map((group, groupIndex) => { - let isMissing = false; + let errorText: string = ''; if (!isEmptyLayer) { if (group.requiredMinDimensionCount) { - isMissing = group.accessors.length < group.requiredMinDimensionCount; - } else if (group.required) { - isMissing = group.accessors.length === 0; - } - } - - const isMissingError = group.requiredMinDimensionCount - ? i18n.translate('xpack.lens.editorFrame.requiresTwoOrMoreFieldsWarningLabel', { - defaultMessage: 'Requires {requiredMinDimensionCount} fields', - values: { - requiredMinDimensionCount: group.requiredMinDimensionCount, - }, - }) - : i18n.translate('xpack.lens.editorFrame.requiresFieldWarningLabel', { + errorText = i18n.translate( + 'xpack.lens.editorFrame.requiresTwoOrMoreFieldsWarningLabel', + { + defaultMessage: 'Requires {requiredMinDimensionCount} fields', + values: { + requiredMinDimensionCount: group.requiredMinDimensionCount, + }, + } + ); + } else if (group.required && group.accessors.length === 0) { + errorText = i18n.translate('xpack.lens.editorFrame.requiresFieldWarningLabel', { defaultMessage: 'Requires field', }); + } else if (group.dimensionsTooMany && group.dimensionsTooMany > 0) { + errorText = i18n.translate( + 'xpack.lens.editorFrame.tooManyDimensionsSingularWarningLabel', + { + defaultMessage: + 'Please remove {dimensionsTooMany, plural, one {a dimension} other {{dimensionsTooMany} dimensions}}', + values: { + dimensionsTooMany: group.dimensionsTooMany, + }, + } + ); + } + } const isOptional = !group.required && !group.suggestedValue; return ( <EuiFormRow @@ -425,8 +435,8 @@ export function LayerPanel( } labelType="legend" key={group.groupId} - isInvalid={isMissing} - error={isMissing ? isMissingError : []} + isInvalid={Boolean(errorText)} + error={errorText} > <> {group.accessors.length ? ( diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx index 863bc82485b81e..9eb79190d44e78 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx @@ -1415,4 +1415,88 @@ describe('embeddable', () => { expect(expressionRenderer).toHaveBeenCalledTimes(2); expect(expressionRenderer.mock.calls[1][0]!.expression).toBe(`edited`); }); + + it('should override noPadding in the display options if noPadding is set in the embeddable input', async () => { + expressionRenderer = jest.fn((_) => null); + + const visDocument: Document = { + state: { + visualization: {}, + datasourceStates: {}, + query: { query: '', language: 'lucene' }, + filters: [], + }, + references: [], + title: 'My title', + visualizationType: 'testVis', + }; + + const createEmbeddable = (noPadding?: boolean) => { + return new Embeddable( + { + timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, + attributeService: attributeServiceMockFromSavedVis(visDocument), + data: dataMock, + expressionRenderer, + basePath, + dataViews: {} as DataViewsContract, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + discover: {}, + navLinks: {}, + }, + inspector: inspectorPluginMock.createStartContract(), + getTrigger, + theme: themeServiceMock.createStartContract(), + visualizationMap: { + [visDocument.visualizationType as string]: { + getDisplayOptions: () => ({ + noPadding: false, + }), + } as unknown as Visualization, + }, + datasourceMap: {}, + injectFilterReferences: jest.fn(mockInjectFilterReferences), + documentToExpression: () => + Promise.resolve({ + ast: { + type: 'expression', + chain: [ + { type: 'function', function: 'my', arguments: {} }, + { type: 'function', function: 'expression', arguments: {} }, + ], + }, + errors: undefined, + }), + uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, + }, + { + timeRange: { + from: 'now-15m', + to: 'now', + }, + noPadding, + } as LensEmbeddableInput + ); + }; + + let embeddable = createEmbeddable(); + embeddable.render(mountpoint); + + // wait one tick to give embeddable time to initialize + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(expressionRenderer).toHaveBeenCalledTimes(1); + expect(expressionRenderer.mock.calls[0][0]!.padding).toBe('s'); + + embeddable = createEmbeddable(true); + embeddable.render(mountpoint); + + // wait one tick to give embeddable time to initialize + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(expressionRenderer).toHaveBeenCalledTimes(2); + expect(expressionRenderer.mock.calls[1][0]!.padding).toBe(undefined); + }); }); diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 0e4c0594db3c47..fb7d7646871c70 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -103,6 +103,7 @@ interface LensBaseEmbeddableInput extends EmbeddableInput { renderMode?: RenderMode; style?: React.CSSProperties; className?: string; + noPadding?: boolean; onBrushEnd?: (data: BrushTriggerEvent['data']) => void; onLoad?: (isLoading: boolean, adapters?: Partial<DefaultInspectorAdapters>) => void; onFilter?: (data: ClickTriggerEvent['data']) => void; @@ -1016,6 +1017,17 @@ export class Embeddable ) { return; } - return this.deps.visualizationMap[this.savedVis.visualizationType].getDisplayOptions!(); + + let displayOptions = + this.deps.visualizationMap[this.savedVis.visualizationType].getDisplayOptions!(); + + if (this.input.noPadding !== undefined) { + displayOptions = { + ...displayOptions, + noPadding: this.input.noPadding, + }; + } + + return displayOptions; } } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx index 1c68844079fa63..059170d9702d86 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx @@ -4,35 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useState, useCallback } from 'react'; -import { i18n } from '@kbn/i18n'; +import React, { useCallback } from 'react'; import { isEqual } from 'lodash'; -import { - EuiLink, - EuiPanel, - EuiPopover, - EuiFormRow, - EuiFlexItem, - EuiFlexGroup, - EuiPopoverProps, - EuiIconTip, -} from '@elastic/eui'; import type { Query } from '@kbn/es-query'; import { GenericIndexPatternColumn, operationDefinitionMap } from '../operations'; import type { IndexPatternLayer } from '../types'; -import { QueryInput, useDebouncedValue, validateQuery } from '../../shared_components'; +import { validateQuery, FilterQueryInput } from '../../shared_components'; import type { IndexPattern } from '../../types'; -const filterByLabel = i18n.translate('xpack.lens.indexPattern.filterBy.label', { - defaultMessage: 'Filter by', -}); - -// to do: get the language from uiSettings -export const defaultFilter: Query = { - query: '', - language: 'kuery', -}; - export function setFilter(columnId: string, layer: IndexPatternLayer, query: Query | undefined) { return { ...layer, @@ -71,18 +50,6 @@ export function Filtering({ }, [columnId, indexPattern, inputFilter, layer, updateLayer] ); - const { inputValue: queryInput, handleInputChange: setQueryInput } = useDebouncedValue<Query>({ - value: inputFilter ?? defaultFilter, - onChange, - }); - const [filterPopoverOpen, setFilterPopoverOpen] = useState(false); - - const onClosePopup: EuiPopoverProps['closePopover'] = useCallback(() => { - setFilterPopoverOpen(false); - if (inputFilter) { - setQueryInput(inputFilter); - } - }, [inputFilter, setQueryInput]); const selectedOperation = operationDefinitionMap[selectedColumn.operationType]; @@ -90,84 +57,12 @@ export function Filtering({ return null; } - const { isValid: isInputFilterValid } = validateQuery(inputFilter, indexPattern); - const { isValid: isQueryInputValid, error: queryInputError } = validateQuery( - queryInput, - indexPattern - ); - - const labelNode = helpMessage ? ( - <> - {filterByLabel}{' '} - <EuiIconTip - color="subdued" - content={helpMessage} - iconProps={{ - className: 'eui-alignTop', - }} - position="top" - size="s" - type="questionInCircle" - /> - </> - ) : ( - filterByLabel - ); - return ( - <EuiFormRow display="rowCompressed" label={labelNode} fullWidth isInvalid={!isInputFilterValid}> - <EuiFlexGroup gutterSize="s" alignItems="center"> - <EuiFlexItem> - <EuiPopover - isOpen={filterPopoverOpen} - closePopover={onClosePopup} - anchorClassName="eui-fullWidth" - panelClassName="lnsIndexPatternDimensionEditor__filtersEditor" - button={ - <EuiPanel paddingSize="none" hasShadow={false} hasBorder> - <EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}> - <EuiFlexItem grow={false}>{/* Empty for spacing */}</EuiFlexItem> - <EuiFlexItem grow={true}> - <EuiLink - className="lnsFiltersOperation__popoverButton" - data-test-subj="indexPattern-filters-existingFilterTrigger" - onClick={() => { - setFilterPopoverOpen(!filterPopoverOpen); - }} - color={isInputFilterValid ? 'text' : 'danger'} - title={i18n.translate('xpack.lens.indexPattern.filterBy.clickToEdit', { - defaultMessage: 'Click to edit', - })} - > - {inputFilter?.query || - i18n.translate('xpack.lens.indexPattern.filterBy.emptyFilterQuery', { - defaultMessage: '(empty)', - })} - </EuiLink> - </EuiFlexItem> - </EuiFlexGroup> - </EuiPanel> - } - > - <EuiFormRow - label={filterByLabel} - isInvalid={!isQueryInputValid} - error={queryInputError} - fullWidth={true} - data-test-subj="indexPattern-filter-by-input" - > - <QueryInput - indexPatternTitle={indexPattern.title} - disableAutoFocus={true} - value={queryInput} - onChange={setQueryInput} - isInvalid={!isQueryInputValid} - onSubmit={() => {}} - /> - </EuiFormRow> - </EuiPopover> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFormRow> + <FilterQueryInput + helpMessage={helpMessage} + onChange={onChange} + indexPattern={indexPattern} + inputFilter={inputFilter} + /> ); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx index 930864091c2eb8..63696a3a2196c1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx @@ -9,8 +9,6 @@ import { EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { EuiComboBox } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; - -import type { Query } from '@kbn/es-query'; import { DatatableUtilitiesService, parseTimeShift } from '@kbn/data-plugin/common'; import { adjustTimeScaleLabelSuffix, @@ -26,12 +24,6 @@ import { } from '../time_shift_utils'; import type { IndexPattern } from '../../types'; -// to do: get the language from uiSettings -export const defaultFilter: Query = { - query: '', - language: 'kuery', -}; - export function setTimeShift( columnId: string, layer: IndexPatternLayer, diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.ts index a059be6db3bfab..4ee70b9480d173 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts @@ -7,7 +7,6 @@ import { Filter, Query } from '@kbn/es-query'; import { - SavedObjectAttributes, SavedObjectsClientContract, SavedObjectReference, ResolvedSimpleSavedObject, @@ -56,9 +55,7 @@ export class SavedObjectIndexStore implements SavedObjectStore { save = async (vis: Document) => { const { savedObjectId, type, references, ...rest } = vis; - // TODO: SavedObjectAttributes should support this kind of object, - // remove this workaround when SavedObjectAttributes is updated. - const attributes = rest as unknown as SavedObjectAttributes; + const attributes = rest; const result = await this.client.create( DOC_TYPE, diff --git a/x-pack/plugins/lens/public/persistence/saved_objects_utils/find_object_by_title.ts b/x-pack/plugins/lens/public/persistence/saved_objects_utils/find_object_by_title.ts index b556f960d6e242..1fda36f4ada364 100644 --- a/x-pack/plugins/lens/public/persistence/saved_objects_utils/find_object_by_title.ts +++ b/x-pack/plugins/lens/public/persistence/saved_objects_utils/find_object_by_title.ts @@ -5,14 +5,10 @@ * 2.0. */ -import type { - SavedObjectsClientContract, - SimpleSavedObject, - SavedObjectAttributes, -} from '@kbn/core/public'; +import type { SavedObjectsClientContract, SimpleSavedObject } from '@kbn/core/public'; /** Returns an object matching a given title */ -export async function findObjectByTitle<T extends SavedObjectAttributes>( +export async function findObjectByTitle<T>( savedObjectsClient: SavedObjectsClientContract, type: string, title: string diff --git a/x-pack/plugins/lens/public/shared_components/filter_query_input.tsx b/x-pack/plugins/lens/public/shared_components/filter_query_input.tsx new file mode 100644 index 00000000000000..db585f5f282048 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/filter_query_input.tsx @@ -0,0 +1,143 @@ +/* + * 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, { useCallback, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiLink, + EuiPanel, + EuiPopover, + EuiFormRow, + EuiFlexItem, + EuiFlexGroup, + EuiIconTip, + EuiPopoverProps, +} from '@elastic/eui'; +import type { Query } from '@kbn/es-query'; +import { QueryInput, useDebouncedValue, validateQuery } from '.'; +import type { IndexPattern } from '../types'; + +const filterByLabel = i18n.translate('xpack.lens.indexPattern.filterBy.label', { + defaultMessage: 'Filter by', +}); + +// to do: get the language from uiSettings +export const defaultFilter: Query = { + query: '', + language: 'kuery', +}; + +export function FilterQueryInput({ + inputFilter, + onChange, + indexPattern, + helpMessage, + label = filterByLabel, + initiallyOpen, +}: { + inputFilter: Query | undefined; + onChange: (query: Query) => void; + indexPattern: IndexPattern; + helpMessage?: string | null; + label?: string; + initiallyOpen?: boolean; +}) { + const [filterPopoverOpen, setFilterPopoverOpen] = useState(Boolean(initiallyOpen)); + const { inputValue: queryInput, handleInputChange: setQueryInput } = useDebouncedValue<Query>({ + value: inputFilter ?? defaultFilter, + onChange, + }); + + const onClosePopup: EuiPopoverProps['closePopover'] = useCallback(() => { + setFilterPopoverOpen(false); + }, []); + + const { isValid: isInputFilterValid } = validateQuery(inputFilter, indexPattern); + const { isValid: isQueryInputValid, error: queryInputError } = validateQuery( + queryInput, + indexPattern + ); + + return ( + <EuiFormRow + display="rowCompressed" + label={ + helpMessage ? ( + <> + {label}{' '} + <EuiIconTip + color="subdued" + content={helpMessage} + iconProps={{ + className: 'eui-alignTop', + }} + position="top" + size="s" + type="questionInCircle" + /> + </> + ) : ( + label + ) + } + fullWidth + isInvalid={!isInputFilterValid} + > + <EuiFlexGroup gutterSize="s" alignItems="center"> + <EuiFlexItem> + <EuiPopover + isOpen={filterPopoverOpen} + closePopover={onClosePopup} + anchorClassName="eui-fullWidth" + panelClassName="lnsIndexPatternDimensionEditor__filtersEditor" + button={ + <EuiPanel paddingSize="none" hasShadow={false} hasBorder> + <EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}> + <EuiFlexItem grow={false}>{/* Empty for spacing */}</EuiFlexItem> + <EuiFlexItem grow={true}> + <EuiLink + className="lnsFiltersOperation__popoverButton" + data-test-subj="indexPattern-filters-existingFilterTrigger" + onClick={() => { + setFilterPopoverOpen(!filterPopoverOpen); + }} + color={isInputFilterValid ? 'text' : 'danger'} + title={i18n.translate('xpack.lens.indexPattern.filterBy.clickToEdit', { + defaultMessage: 'Click to edit', + })} + > + {inputFilter?.query || + i18n.translate('xpack.lens.indexPattern.filterBy.emptyFilterQuery', { + defaultMessage: '(empty)', + })} + </EuiLink> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + } + > + <EuiFormRow + label={label} + isInvalid={!isQueryInputValid} + error={queryInputError} + fullWidth={true} + data-test-subj="indexPattern-filter-by-input" + > + <QueryInput + indexPatternTitle={indexPattern.title} + disableAutoFocus={true} + value={queryInput} + onChange={setQueryInput} + isInvalid={!isQueryInputValid} + onSubmit={() => {}} + /> + </EuiFormRow> + </EuiPopover> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFormRow> + ); +} diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts index 924f678c1f96b5..3f30eb64ff2c93 100644 --- a/x-pack/plugins/lens/public/shared_components/index.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -36,5 +36,6 @@ export { NameInput } from './name_input'; export { ValueLabelsSettings } from './value_labels_settings'; export { AxisTitleSettings } from './axis_title_settings'; export { DimensionEditorSection } from './dimension_section'; +export { FilterQueryInput } from './filter_query_input'; export * from './static_header'; export * from './vis_label'; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 17950f8c5e50e6..a292d50cab8953 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -712,6 +712,7 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & { groupId: string; accessors: AccessorConfig[]; supportsMoreColumns: boolean; + dimensionsTooMany?: number; /** If required, a warning will appear if accessors are empty */ required?: boolean; requiredMinDimensionCount?: number; diff --git a/x-pack/plugins/lens/public/visualizations/partition/to_expression.ts b/x-pack/plugins/lens/public/visualizations/partition/to_expression.ts index 5aeb9807b1eb34..3f66965dc98b6c 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/to_expression.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/to_expression.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { Ast } from '@kbn/interpreter'; +import type { Ast, AstFunction } from '@kbn/interpreter'; import { Position } from '@elastic/charts'; import type { PaletteOutput, PaletteRegistry } from '@kbn/coloring'; @@ -23,6 +23,7 @@ import { LegendDisplay, } from '../../../common'; import { getDefaultVisualValuesForLayer } from '../../shared_components/datasource_default_values'; +import { isCollapsed } from './visualization'; interface Attributes { isPreview: boolean; @@ -142,7 +143,10 @@ const generateCommonArguments: GenerateExpressionAstArguments = ( ) => { return { labels: generateCommonLabelsAstArgs(state, attributes, layer), - buckets: operations.map((o) => o.columnId).map(prepareDimension), + buckets: operations + .filter(({ columnId }) => !isCollapsed(columnId, layer)) + .map(({ columnId }) => columnId) + .map(prepareDimension), metric: layer.metric ? [prepareDimension(layer.metric)] : [], legendDisplay: [attributes.isPreview ? LegendDisplay.HIDE : layer.legendDisplay], legendPosition: [layer.legendPosition || Position.Right], @@ -218,6 +222,7 @@ const generateMosaicVisAst: GenerateExpressionAstFunction = (...rest) => ({ ...generateCommonArguments(...rest), // flip order of bucket dimensions so the rows are fetched before the columns to keep them stable buckets: rest[2] + .filter(({ columnId }) => !isCollapsed(columnId, rest[3])) .reverse() .map((o) => o.columnId) .map(prepareDimension), @@ -298,6 +303,19 @@ function expressionHelper( type: 'expression', chain: [ ...(datasourceAst ? datasourceAst.chain : []), + ...groups + .filter((columnId) => layer.collapseFns?.[columnId]) + .map((columnId) => { + return { + type: 'function', + function: 'lens_collapse', + arguments: { + by: groups.filter((chk) => chk !== columnId), + metric: [layer.metric], + fn: [layer.collapseFns![columnId]!], + }, + } as AstFunction; + }), ...(visualizationAst ? visualizationAst.chain : []), ], }; diff --git a/x-pack/plugins/lens/public/visualizations/partition/toolbar.tsx b/x-pack/plugins/lens/public/visualizations/partition/toolbar.tsx index c6b783298e3cd1..4dcb01c3e93f92 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/toolbar.tsx +++ b/x-pack/plugins/lens/public/visualizations/partition/toolbar.tsx @@ -31,6 +31,8 @@ import { } from '../../shared_components'; import { getDefaultVisualValuesForLayer } from '../../shared_components/datasource_default_values'; import { shouldShowValuesInLegend } from './render_helpers'; +import { CollapseSetting } from '../../shared_components/collapse_setting'; +import { isCollapsed } from './visualization'; const legendOptions: Array<{ value: SharedPieLayerState['legendDisplay']; @@ -306,14 +308,46 @@ export function DimensionEditor( paletteService: PaletteRegistry; } ) { - if (props.accessor !== Object.values(props.state.layers)[0].primaryGroups[0]) return null; + const currentLayer = props.state.layers.find((layer) => layer.layerId === props.layerId); + + if (!currentLayer) { + return null; + } + + const firstNonCollapsedColumnId = currentLayer.primaryGroups.find( + (columnId) => !isCollapsed(columnId, currentLayer) + ); + return ( - <PalettePicker - palettes={props.paletteService} - activePalette={props.state.palette} - setPalette={(newPalette) => { - props.setState({ ...props.state, palette: newPalette }); - }} - /> + <> + {props.accessor === firstNonCollapsedColumnId && ( + <PalettePicker + palettes={props.paletteService} + activePalette={props.state.palette} + setPalette={(newPalette) => { + props.setState({ ...props.state, palette: newPalette }); + }} + /> + )} + <CollapseSetting + value={currentLayer?.collapseFns?.[props.accessor] || ''} + onChange={(collapseFn: string) => { + props.setState({ + ...props.state, + layers: props.state.layers.map((layer) => + layer.layerId !== props.layerId + ? layer + : { + ...layer, + collapseFns: { + ...layer.collapseFns, + [props.accessor]: collapseFn, + }, + } + ), + }); + }} + /> + </> ); } diff --git a/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts index b89c352cb506e4..82073f1d5d60f1 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts @@ -18,6 +18,8 @@ import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { createMockDatasource, createMockFramePublicAPI } from '../../mocks'; import { FramePublicAPI } from '../../types'; import { themeServiceMock } from '@kbn/core/public/mocks'; +import { cloneDeep } from 'lodash'; +import { PartitionChartsMeta } from './partition_charts_meta'; jest.mock('../../id_generator'); @@ -59,10 +61,26 @@ function mockFrame(): FramePublicAPI { // Just a basic bootstrap here to kickstart the tests describe('pie_visualization', () => { describe('#getErrorMessages', () => { - it('returns undefined if no error is raised', () => { - const error = pieVisualization.getErrorMessages(getExampleState()); + describe('too many dimensions', () => { + const state = { ...getExampleState(), shape: PieChartTypes.MOSAIC }; + const colIds = new Array(PartitionChartsMeta.mosaic.maxBuckets + 1) + .fill(undefined) + .map((_, i) => String(i + 1)); - expect(error).not.toBeDefined(); + state.layers[0].primaryGroups = colIds.slice(0, 2); + state.layers[0].secondaryGroups = colIds.slice(2); + + it('returns error', () => { + expect(pieVisualization.getErrorMessages(state)).toHaveLength(1); + }); + + it("doesn't count collapsed dimensions", () => { + state.layers[0].collapseFns = { + [colIds[0]]: 'some-fn', + }; + + expect(pieVisualization.getErrorMessages(state)).toHaveLength(0); + }); }); }); @@ -111,4 +129,147 @@ describe('pie_visualization', () => { ); }); }); + + describe('#removeDimension', () => { + it('removes corresponding collapse function if exists', () => { + const state = getExampleState(); + + const colIds = ['1', '2', '3', '4']; + + state.layers[0].primaryGroups = colIds; + + state.layers[0].collapseFns = { + '1': 'sum', + '3': 'max', + }; + + const newState = pieVisualization.removeDimension({ + layerId: LAYER_ID, + columnId: '3', + prevState: state, + frame: mockFrame(), + }); + + expect(newState.layers[0].collapseFns).not.toHaveProperty('3'); + }); + }); + + describe('#getConfiguration', () => { + it('assigns correct icons to accessors', () => { + const colIds = ['1', '2', '3', '4']; + + const frame = mockFrame(); + frame.datasourceLayers[LAYER_ID]!.getTableSpec = () => + colIds.map((id) => ({ columnId: id, fields: [] })); + + const state = getExampleState(); + state.layers[0].primaryGroups = colIds; + state.layers[0].collapseFns = { + '1': 'sum', + '3': 'max', + }; + const configuration = pieVisualization.getConfiguration({ + state, + frame, + layerId: state.layers[0].layerId, + }); + + // palette should be assigned to the first non-collapsed dimension + expect(configuration.groups[0].accessors).toMatchInlineSnapshot(` + Array [ + Object { + "columnId": "1", + "triggerIcon": "aggregate", + }, + Object { + "columnId": "2", + "palette": Array [ + "red", + "black", + ], + "triggerIcon": "colorBy", + }, + Object { + "columnId": "3", + "triggerIcon": "aggregate", + }, + Object { + "columnId": "4", + "triggerIcon": undefined, + }, + ] + `); + + const mosaicState = getExampleState(); + mosaicState.shape = PieChartTypes.MOSAIC; + mosaicState.layers[0].primaryGroups = colIds.slice(0, 2); + mosaicState.layers[0].secondaryGroups = colIds.slice(2); + mosaicState.layers[0].collapseFns = { + '1': 'sum', + '3': 'max', + }; + const mosaicConfiguration = pieVisualization.getConfiguration({ + state: mosaicState, + frame, + layerId: mosaicState.layers[0].layerId, + }); + + expect(mosaicConfiguration.groups.map(({ accessors }) => accessors)).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "columnId": "1", + "triggerIcon": "aggregate", + }, + Object { + "columnId": "2", + "palette": Array [ + "red", + "black", + ], + "triggerIcon": "colorBy", + }, + ], + Array [ + Object { + "columnId": "3", + "triggerIcon": "aggregate", + }, + Object { + "columnId": "4", + "triggerIcon": undefined, + }, + ], + Array [], + ] + `); + }); + + it("doesn't count collapsed columns toward the dimension limits", () => { + const colIds = new Array(PartitionChartsMeta.pie.maxBuckets) + .fill(undefined) + .map((_, i) => String(i + 1)); + + const frame = mockFrame(); + frame.datasourceLayers[LAYER_ID]!.getTableSpec = () => + colIds.map((id) => ({ columnId: id, fields: [] })); + + const state = getExampleState(); + state.layers[0].primaryGroups = colIds; + + const getConfig = (_state: PieVisualizationState) => + pieVisualization.getConfiguration({ + state: _state, + frame, + layerId: state.layers[0].layerId, + }); + + expect(getConfig(state).groups[0].supportsMoreColumns).toBeFalsy(); + + const stateWithCollapsed = cloneDeep(state); + stateWithCollapsed.layers[0].collapseFns = { '1': 'sum' }; + + expect(getConfig(stateWithCollapsed).groups[0].supportsMoreColumns).toBeTruthy(); + }); + }); }); diff --git a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx index 49fc204fe5591f..6a58d46b34caab 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx @@ -13,6 +13,7 @@ import type { PaletteRegistry } from '@kbn/coloring'; import { ThemeServiceStart } from '@kbn/core/public'; import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public'; +import { EuiSpacer } from '@elastic/eui'; import type { Visualization, OperationMetadata, @@ -45,18 +46,28 @@ const bucketedOperations = (op: OperationMetadata) => op.isBucketed; const numberMetricOperations = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number' && !op.isStaticValue; +export const isCollapsed = (columnId: string, layer: PieLayerState) => + Boolean(layer.collapseFns?.[columnId]); + const applyPaletteToColumnConfig = ( columns: AccessorConfig[], - { palette }: PieVisualizationState, + layer: PieLayerState, + palette: PieVisualizationState['palette'], paletteService: PaletteRegistry ) => { - columns[0] = { - columnId: columns[0].columnId, - triggerIcon: 'colorBy', - palette: paletteService - .get(palette?.name || 'default') - .getCategoricalColors(10, palette?.params), - }; + const firstNonCollapsedColumnIdx = columns.findIndex( + (column) => !isCollapsed(column.columnId, layer) + ); + + if (firstNonCollapsedColumnIdx > -1) { + columns[firstNonCollapsedColumnIdx] = { + columnId: columns[firstNonCollapsedColumnIdx].columnId, + triggerIcon: 'colorBy', + palette: paletteService + .get(palette?.name || 'default') + .getCategoricalColors(10, palette?.params), + }; + } }; export const getPieVisualization = ({ @@ -129,10 +140,11 @@ export const getPieVisualization = ({ // When we add a column it could be empty, and therefore have no order const accessors: AccessorConfig[] = originalOrder.map((accessor) => ({ columnId: accessor, + triggerIcon: isCollapsed(accessor, layer) ? ('aggregate' as const) : undefined, })); if (accessors.length) { - applyPaletteToColumnConfig(accessors, state, paletteService); + applyPaletteToColumnConfig(accessors, layer, state.palette, paletteService); } const primaryGroupConfigBaseProps = { @@ -143,6 +155,11 @@ export const getPieVisualization = ({ filterOperations: bucketedOperations, }; + const totalNonCollapsedAccessors = accessors.reduce( + (total, { columnId }) => total + (isCollapsed(columnId, layer) ? 0 : 1), + 0 + ); + switch (state.shape) { case 'donut': case 'pie': @@ -154,7 +171,8 @@ export const getPieVisualization = ({ dimensionEditorGroupLabel: i18n.translate('xpack.lens.pie.sliceDimensionGroupLabel', { defaultMessage: 'Slice', }), - supportsMoreColumns: accessors.length < PartitionChartsMeta.pie.maxBuckets, + supportsMoreColumns: totalNonCollapsedAccessors < PartitionChartsMeta.pie.maxBuckets, + dimensionsTooMany: totalNonCollapsedAccessors - PartitionChartsMeta.pie.maxBuckets, dataTestSubj: 'lnsPie_sliceByDimensionPanel', }; case 'mosaic': @@ -166,7 +184,8 @@ export const getPieVisualization = ({ dimensionEditorGroupLabel: i18n.translate('xpack.lens.pie.verticalAxisDimensionLabel', { defaultMessage: 'Vertical axis', }), - supportsMoreColumns: accessors.length === 0, + supportsMoreColumns: totalNonCollapsedAccessors === 0, + dimensionsTooMany: totalNonCollapsedAccessors - 1, dataTestSubj: 'lnsPie_verticalAxisDimensionPanel', }; default: @@ -178,7 +197,10 @@ export const getPieVisualization = ({ dimensionEditorGroupLabel: i18n.translate('xpack.lens.pie.treemapDimensionGroupLabel', { defaultMessage: 'Group', }), - supportsMoreColumns: accessors.length < PartitionChartsMeta[state.shape].maxBuckets, + supportsMoreColumns: + totalNonCollapsedAccessors < PartitionChartsMeta[state.shape].maxBuckets, + dimensionsTooMany: + totalNonCollapsedAccessors - PartitionChartsMeta[state.shape].maxBuckets, dataTestSubj: 'lnsPie_groupByDimensionPanel', }; } @@ -188,6 +210,7 @@ export const getPieVisualization = ({ const originalSecondaryOrder = getSortedGroups(datasource, layer, 'secondaryGroups'); const accessors = originalSecondaryOrder.map((accessor) => ({ columnId: accessor, + triggerIcon: isCollapsed(accessor, layer) ? ('aggregate' as const) : undefined, })); const secondaryGroupConfigBaseProps = { @@ -198,6 +221,11 @@ export const getPieVisualization = ({ filterOperations: bucketedOperations, }; + const totalNonCollapsedAccessors = accessors.reduce( + (total, { columnId }) => total + (isCollapsed(columnId, layer) ? 0 : 1), + 0 + ); + switch (state.shape) { case 'mosaic': return { @@ -211,7 +239,8 @@ export const getPieVisualization = ({ defaultMessage: 'Horizontal axis', } ), - supportsMoreColumns: accessors.length === 0, + supportsMoreColumns: totalNonCollapsedAccessors === 0, + dimensionsTooMany: totalNonCollapsedAccessors - 1, dataTestSubj: 'lnsPie_horizontalAxisDimensionPanel', }; default: @@ -280,13 +309,21 @@ export const getPieVisualization = ({ return l; } - if (l.metric === columnId) { - return { ...l, metric: undefined }; + const newLayer = { ...l }; + + if (l.collapseFns?.[columnId]) { + const newCollapseFns = { ...l.collapseFns }; + delete newCollapseFns[columnId]; + newLayer.collapseFns = newCollapseFns; + } + + if (newLayer.metric === columnId) { + return { ...newLayer, metric: undefined }; } return { - ...l, - primaryGroups: l.primaryGroups.filter((c) => c !== columnId), - secondaryGroups: l.secondaryGroups?.filter((c) => c !== columnId) ?? undefined, + ...newLayer, + primaryGroups: newLayer.primaryGroups.filter((c) => c !== columnId), + secondaryGroups: newLayer.secondaryGroups?.filter((c) => c !== columnId) ?? undefined, }; }), }; @@ -386,7 +423,35 @@ export const getPieVisualization = ({ }, getErrorMessages(state) { - // not possible to break it? - return undefined; + const hasTooManyBucketDimensions = state.layers + .map( + (layer) => + Array.from(new Set([...layer.primaryGroups, ...(layer.secondaryGroups ?? [])])).filter( + (columnId) => !isCollapsed(columnId, layer) + ).length > PartitionChartsMeta[state.shape].maxBuckets + ) + .some(Boolean); + + return hasTooManyBucketDimensions + ? [ + { + shortMessage: i18n.translate('xpack.lens.pie.tooManyDimensions', { + defaultMessage: 'Your visualization has too many dimensions.', + }), + longMessage: ( + <span> + {i18n.translate('xpack.lens.pie.tooManyDimensionsLong', { + defaultMessage: + 'Your visualization has too many dimensions. Please follow the instructions in the layer panel.', + })} + <EuiSpacer size="s" /> + {i18n.translate('xpack.lens.pie.collapsedDimensionsDontCount', { + defaultMessage: "(Collapsed dimensions don't count toward this limit.)", + })} + </span> + ), + }, + ] + : []; }, }); diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/annotations_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/annotations_panel.tsx index 96f31e2e8754f0..5d68e29a88d080 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/annotations_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/annotations_panel.tsx @@ -6,7 +6,7 @@ */ import './index.scss'; -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiSwitch, EuiSwitchEvent, EuiButtonGroup, EuiSpacer } from '@elastic/eui'; import type { PaletteRegistry } from '@kbn/coloring'; @@ -31,7 +31,7 @@ import { useDebouncedValue, } from '../../../../shared_components'; import { isHorizontalChart } from '../../state_helpers'; -import { defaultAnnotationLabel } from '../../annotations/helpers'; +import { defaultAnnotationLabel, defaultRangeAnnotationLabel } from '../../annotations/helpers'; import { ColorPicker } from '../color_picker'; import { IconSelectSetting, TextDecorationSetting } from '../shared/marker_decoration_settings'; import { LineStyleSettings } from '../shared/line_style_settings'; @@ -42,7 +42,7 @@ import type { State, XYState, XYAnnotationLayerConfig } from '../../types'; import { ConfigPanelManualAnnotation } from './manual_annotation_panel'; import { ConfigPanelQueryAnnotation } from './query_annotation_panel'; import { TooltipSection } from './tooltip_annotation_panel'; -import { sanitizeProperties } from './helpers'; +import { sanitizeProperties, toLineAnnotationColor } from './helpers'; export const AnnotationsPanel = ( props: VisualizationDimensionEditorProps<State> & { @@ -68,6 +68,14 @@ export const AnnotationsPanel = ( const isQueryBased = isQueryAnnotationConfig(currentAnnotation); const isRange = isRangeAnnotationConfig(currentAnnotation); + const [queryInputShouldOpen, setQueryInputShouldOpen] = React.useState(false); + useEffect(() => { + if (isQueryBased) { + setQueryInputShouldOpen(false); + } else { + setQueryInputShouldOpen(true); + } + }, [isQueryBased]); const setAnnotations = useCallback( (annotation) => { @@ -114,11 +122,11 @@ export const AnnotationsPanel = ( buttonSize="compressed" options={[ { - id: `lens_xyChart_annotation_staticDate`, - label: i18n.translate('xpack.lens.xyChart.annotation.staticDate', { + id: `lens_xyChart_annotation_manual`, + label: i18n.translate('xpack.lens.xyChart.annotation.manual', { defaultMessage: 'Static Date', }), - 'data-test-subj': 'lnsXY_annotation_staticDate', + 'data-test-subj': 'lnsXY_annotation_manual', }, { id: `lens_xyChart_annotation_query`, @@ -128,18 +136,28 @@ export const AnnotationsPanel = ( 'data-test-subj': 'lnsXY_annotation_query', }, ]} - idSelected={`lens_xyChart_annotation_${ - currentAnnotation?.type === 'query' ? 'query' : 'staticDate' - }`} + idSelected={`lens_xyChart_annotation_${currentAnnotation?.type}`} onChange={(id) => { - setAnnotations({ - type: id === `lens_xyChart_annotation_query` ? 'query' : 'manual', - // when switching to query, reset the key value - key: - !isQueryBased && id === `lens_xyChart_annotation_query` - ? { type: 'point_in_time' } - : currentAnnotation?.key, - }); + const typeFromId = id.replace('lens_xyChart_annotation_', ''); + if (currentAnnotation?.type === typeFromId) { + return; + } + if (currentAnnotation?.key.type === 'range') { + setAnnotations({ + type: typeFromId, + label: + currentAnnotation.label === defaultRangeAnnotationLabel + ? defaultAnnotationLabel + : currentAnnotation.label, + color: toLineAnnotationColor(currentAnnotation.color), + key: { type: 'point_in_time' }, + }); + } else { + setAnnotations({ + type: typeFromId, + key: currentAnnotation?.key, + }); + } }} isFullWidth /> @@ -151,6 +169,7 @@ export const AnnotationsPanel = ( frame={frame} state={state} layer={localLayer} + queryInputShouldOpen={queryInputShouldOpen} /> ) : ( <ConfigPanelManualAnnotation @@ -245,6 +264,7 @@ export const AnnotationsPanel = ( } }} fieldIsInvalid={!fieldIsValid} + autoFocus={!selectedField} /> </> ); diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx index c9cd0bbec7d350..69cd398c562bf8 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx @@ -15,8 +15,7 @@ import { FieldOption, FieldOptionValue, FieldPicker, - QueryInput, - validateQuery, + FilterQueryInput, } from '../../../../shared_components'; import type { FramePublicAPI } from '../../../../types'; import type { XYState, XYAnnotationLayerConfig } from '../../types'; @@ -32,14 +31,15 @@ export const ConfigPanelQueryAnnotation = ({ state, onChange, layer, + queryInputShouldOpen, }: { annotation?: QueryPointEventAnnotationConfig; onChange: (annotations: Partial<QueryPointEventAnnotationConfig> | undefined) => void; frame: FramePublicAPI; state: XYState; layer: XYAnnotationLayerConfig; + queryInputShouldOpen?: boolean; }) => { - const inputQuery = annotation?.filter ?? defaultQuery; const currentIndexPattern = frame.dataViews.indexPatterns[layer.indexPatternId]; const currentExistingFields = frame.dataViews.existingFields[currentIndexPattern.title]; // list only supported field by operation, remove the rest @@ -58,51 +58,36 @@ export const ConfigPanelQueryAnnotation = ({ 'data-test-subj': `lns-fieldOption-${field.name}`, } as FieldOption<FieldOptionValue>; }); - const { isValid: isQueryInputValid, error: queryInputError } = validateQuery( - annotation?.filter, - currentIndexPattern - ); const selectedField = annotation?.timeField || currentIndexPattern.timeFieldName || options[0]?.value.field; const fieldIsValid = selectedField ? Boolean(currentIndexPattern.getFieldByName(selectedField)) : true; + return ( <> <EuiFormRow + hasChildLabel display="rowCompressed" className="lnsRowCompressedMargin" fullWidth label={i18n.translate('xpack.lens.xyChart.annotation.queryInput', { defaultMessage: 'Annotation query', })} - isInvalid={!isQueryInputValid} - error={queryInputError} + data-test-subj="annotation-query-based-query-input" > - <QueryInput - value={inputQuery} - onChange={function (input: Query): void { - onChange({ filter: { type: 'kibana_query', ...input } }); + <FilterQueryInput + initiallyOpen={queryInputShouldOpen} + label="" + inputFilter={annotation?.filter ?? defaultQuery} + onChange={(query: Query) => { + onChange({ filter: { type: 'kibana_query', ...query } }); }} - disableAutoFocus - indexPatternTitle={frame.dataViews.indexPatterns[layer.indexPatternId].title} - isInvalid={!isQueryInputValid || inputQuery.query === ''} - onSubmit={() => {}} - data-test-subj="annotation-query-based-query-input" - placeholder={ - inputQuery.language === 'kuery' - ? i18n.translate('xpack.lens.annotations.query.queryPlaceholderKql', { - defaultMessage: '{example}', - values: { example: 'method : "GET"' }, - }) - : i18n.translate('xpack.lens.annotations.query.queryPlaceholderLucene', { - defaultMessage: '{example}', - values: { example: 'method:GET' }, - }) - } + indexPattern={currentIndexPattern} /> </EuiFormRow> + <EuiFormRow display="rowCompressed" className="lnsRowCompressedMargin" diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx index e4945f42f8089e..c8ea7a0ed2ece7 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx @@ -198,6 +198,7 @@ export function TooltipSection({ onFieldSelectChange(choice, index); }} fieldIsInvalid={!fieldIsValid} + autoFocus={isNew && value == null} /> </EuiFlexItem> <EuiFlexItem grow={false}> diff --git a/x-pack/plugins/lens/readme.md b/x-pack/plugins/lens/readme.md index 47a1d82c36a15d..41db47090cb47d 100644 --- a/x-pack/plugins/lens/readme.md +++ b/x-pack/plugins/lens/readme.md @@ -148,6 +148,8 @@ Example: } ``` +**Important!** To prevent conflicts, it's important to not re-use ad-hoc data view ids for different specs. If you change the spec in some way, make sure to also change its id. This even applies across multiple embeddables, sessions, etc. Ideally, the id will be globally unique. You can use the `uuid` package to generate a new unique id every time when you are changing the spec in some way. However, make sure to also not change the id on every single render either, as this will have a substantial performance impact. + ## Refreshing a Lens embeddable The Lens embeddable is handling data fetching internally, this means as soon as the props change, it will trigger a new request if necessary. However, in some situations it's necessary to trigger a refresh even if the configuration of the chart doesn't change at all. Refreshing is managed using search sessions is Lens. To trigger a refresh without changing the actual configuration of a Lens embeddable, follow these steps: diff --git a/x-pack/plugins/maps/public/classes/fields/agg/percentile_agg_field.test.ts b/x-pack/plugins/maps/public/classes/fields/agg/percentile_agg_field.test.ts index 3b6eff33130776..a00a5d1fcd1866 100644 --- a/x-pack/plugins/maps/public/classes/fields/agg/percentile_agg_field.test.ts +++ b/x-pack/plugins/maps/public/classes/fields/agg/percentile_agg_field.test.ts @@ -46,15 +46,11 @@ const mockEsDocField = { }, }; -const defaultParams = { - source: mockEsAggSource, - origin: FIELD_ORIGIN.SOURCE, -}; - describe('percentile agg field', () => { test('should include percentile in name', () => { const field = new PercentileAggField({ - ...defaultParams, + source: mockEsAggSource, + origin: FIELD_ORIGIN.SOURCE, esDocField: mockEsDocField as ESDocField, percentile: 80, }); @@ -63,7 +59,8 @@ describe('percentile agg field', () => { test('should create percentile dsl', () => { const field = new PercentileAggField({ - ...defaultParams, + source: mockEsAggSource, + origin: FIELD_ORIGIN.SOURCE, esDocField: mockEsDocField as ESDocField, percentile: 80, }); @@ -73,24 +70,84 @@ describe('percentile agg field', () => { }); }); - test('label', async () => { - const field = new PercentileAggField({ - ...defaultParams, - esDocField: mockEsDocField as ESDocField, - percentile: 80, + describe('getLabel', () => { + test('should return percentile in label', async () => { + const field = new PercentileAggField({ + source: mockEsAggSource, + origin: FIELD_ORIGIN.SOURCE, + esDocField: mockEsDocField as ESDocField, + percentile: 80, + }); + + expect(await field.getLabel()).toEqual('80th agg_label'); }); - expect(await field.getLabel()).toEqual('80th agg_label'); + test('should return median for 50th percentile', async () => { + const field = new PercentileAggField({ + source: mockEsAggSource, + origin: FIELD_ORIGIN.SOURCE, + label: '', + esDocField: mockEsDocField as ESDocField, + percentile: 50, + }); + + expect(await field.getLabel()).toEqual('median foobar'); + }); }); - test('label (median)', async () => { - const field = new PercentileAggField({ - ...defaultParams, - label: '', - esDocField: mockEsDocField as ESDocField, - percentile: 50, + describe('getMbFieldName', () => { + test('should return field name when source is not MVT', () => { + const field = new PercentileAggField({ + origin: FIELD_ORIGIN.SOURCE, + source: { + getAggKey: (aggType: AGG_TYPE, fieldName: string) => { + return 'agg_key'; + }, + isMvt: () => { + return false; + }, + } as unknown as IESAggSource, + esDocField: mockEsDocField as ESDocField, + percentile: 80.5, + }); + + expect(field.getMbFieldName()).toEqual('agg_key_80.5'); + }); + + test('should return field name and percentile when source is MVT', () => { + const field = new PercentileAggField({ + origin: FIELD_ORIGIN.SOURCE, + source: { + getAggKey: (aggType: AGG_TYPE, fieldName: string) => { + return 'agg_key'; + }, + isMvt: () => { + return true; + }, + } as unknown as IESAggSource, + esDocField: mockEsDocField as ESDocField, + percentile: 80.5, + }); + + expect(field.getMbFieldName()).toEqual('agg_key_80.5.values.80.5'); }); - expect(await field.getLabel()).toEqual('median foobar'); + test('should return field name and percentile with single decimal place when source is MVT and percentile is interger', () => { + const field = new PercentileAggField({ + origin: FIELD_ORIGIN.SOURCE, + source: { + getAggKey: (aggType: AGG_TYPE, fieldName: string) => { + return 'agg_key'; + }, + isMvt: () => { + return true; + }, + } as unknown as IESAggSource, + esDocField: mockEsDocField as ESDocField, + percentile: 80, + }); + + expect(field.getMbFieldName()).toEqual('agg_key_80.values.80.0'); + }); }); }); diff --git a/x-pack/plugins/maps/public/classes/fields/agg/percentile_agg_field.ts b/x-pack/plugins/maps/public/classes/fields/agg/percentile_agg_field.ts index 57dcd5631918c1..c1e076eacd6202 100644 --- a/x-pack/plugins/maps/public/classes/fields/agg/percentile_agg_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/agg/percentile_agg_field.ts @@ -40,6 +40,13 @@ export class PercentileAggField extends AggField implements IESAggField { return true; } + getMbFieldName(): string { + return this._source.isMvt() + ? this.getName() + + `.values.${this._percentile}${Number.isInteger(this._percentile) ? '.0' : ''}` + : this.getName(); + } + canValueBeFormatted(): boolean { return true; } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx index d61e3e46a11199..a1d29c8db13636 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx @@ -23,7 +23,7 @@ import { TiledSingleLayerVectorSourceDescriptor, VectorLayerDescriptor, } from '../../../../../common/descriptor_types'; -import { SOURCE_TYPES } from '../../../../../common/constants'; +import { LAYER_TYPE, SOURCE_TYPES } from '../../../../../common/constants'; import { MvtVectorLayer } from './mvt_vector_layer'; const defaultConfig = { @@ -63,6 +63,11 @@ function createLayer( return new MvtVectorLayer({ layerDescriptor, source: mvtSource, customIcons: [] }); } +test('should have type MVT_VECTOR_LAYER', () => { + const layer: MvtVectorLayer = createLayer({}, {}); + expect(layer.getType()).toEqual(LAYER_TYPE.MVT_VECTOR); +}); + describe('visiblity', () => { it('should get minzoom from source', async () => { const layer: MvtVectorLayer = createLayer({}, {}); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index 3c78bf954e258b..35a5caa7ff9b8f 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -123,7 +123,8 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { mapColors?: string[] ): VectorLayerDescriptor { const layerDescriptor = super.createDescriptor(options) as VectorLayerDescriptor; - layerDescriptor.type = LAYER_TYPE.GEOJSON_VECTOR; + layerDescriptor.type = + layerDescriptor.type !== undefined ? layerDescriptor.type : LAYER_TYPE.GEOJSON_VECTOR; if (!options.style) { const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []); diff --git a/x-pack/plugins/maps/server/saved_objects/setup_saved_objects.ts b/x-pack/plugins/maps/server/saved_objects/setup_saved_objects.ts index 7bc209b4b1a9fb..caa99d98662c15 100644 --- a/x-pack/plugins/maps/server/saved_objects/setup_saved_objects.ts +++ b/x-pack/plugins/maps/server/saved_objects/setup_saved_objects.ts @@ -19,7 +19,7 @@ export function setupSavedObjects( core: CoreSetup, getFilterMigrations: () => MigrateFunctionsObject ) { - core.savedObjects.registerType({ + core.savedObjects.registerType<MapSavedObjectAttributes>({ name: 'map', hidden: false, namespaceType: 'multiple-isolated', diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx index c77a254be2f10d..9b3f86070a6cb9 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx @@ -10,8 +10,6 @@ import React from 'react'; import { CreateAnalyticsButton } from './create_analytics_button'; -jest.mock('../../../../../../../shared_imports'); - describe('Data Frame Analytics: <CreateAnalyticsButton />', () => { test('Minimal initialization', () => { const wrapper = mount( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index 58a471b4e72468..aa25bf2c8eb3dd 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -7,12 +7,14 @@ import { i18n } from '@kbn/i18n'; import { memoize, isEqual } from 'lodash'; + // @ts-ignore import numeral from '@elastic/numeral'; + import { indexPatterns } from '@kbn/data-plugin/public'; -import { isValidIndexName } from '../../../../../../../common/util/es_utils'; +import { XJson } from '@kbn/es-ui-shared-plugin/public'; -import { collapseLiteralStrings } from '../../../../../../../shared_imports'; +import { isValidIndexName } from '../../../../../../../common/util/es_utils'; import { Action, ACTION } from './actions'; import { @@ -48,6 +50,8 @@ import { } from '../../../../common/analytics'; import { isAdvancedConfig } from '../../components/action_clone/clone_action_name'; +const { collapseLiteralStrings } = XJson; + const mmlAllowedUnitsStr = `${ALLOWED_DATA_UNITS.slice(0, ALLOWED_DATA_UNITS.length - 1).join( ', ' )} or ${[...ALLOWED_DATA_UNITS].pop()}`; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js index 4fa5cdcb7c9f71..9100b7ffa03ab6 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js @@ -32,11 +32,13 @@ import { validateModelMemoryLimit, validateGroupNames, isValidCustomUrls } from import { toastNotificationServiceProvider } from '../../../../services/toast_notification_service'; import { ml } from '../../../../services/ml_api_service'; import { withKibana } from '@kbn/kibana-react-plugin/public'; -import { collapseLiteralStrings } from '../../../../../../shared_imports'; +import { XJson } from '@kbn/es-ui-shared-plugin/public'; import { DATAFEED_STATE, JOB_STATE } from '../../../../../../common/constants/states'; import { isManagedJob } from '../../../jobs_utils'; import { ManagedJobsWarningCallout } from '../confirm_modals/managed_jobs_warning_callout'; +const { collapseLiteralStrings } = XJson; + export class EditJobFlyoutUI extends Component { _initialJobFormState = null; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.tsx index 631b9162025056..bbe91f77a72047 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.tsx @@ -6,13 +6,12 @@ */ import React, { FC } from 'react'; +import { XJsonMode } from '@kbn/ace'; -import { - expandLiteralStrings, - XJsonMode, - EuiCodeEditor, - EuiCodeEditorProps, -} from '../../../../../../shared_imports'; +import { EuiCodeEditor, XJson } from '@kbn/es-ui-shared-plugin/public'; +import type { EuiCodeEditorProps } from '@kbn/es-ui-shared-plugin/public'; + +const { expandLiteralStrings } = XJson; export const ML_EDITOR_MODE = { TEXT: 'text', JSON: 'json', XJSON: new XJsonMode() }; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx index 5b164623004460..b9e77a3a5f2732 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx @@ -20,7 +20,7 @@ import { EuiSpacer, EuiCallOut, } from '@elastic/eui'; -import { collapseLiteralStrings } from '../../../../../../../../shared_imports'; +import { XJson } from '@kbn/es-ui-shared-plugin/public'; import { CombinedJob, Datafeed } from '../../../../../../../../common/types/anomaly_detection_jobs'; import { ML_EDITOR_MODE, MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; import { isValidJson } from '../../../../../../../../common/util/validation_utils'; @@ -29,6 +29,8 @@ import { isAdvancedJobCreator } from '../../../../common/job_creator'; import { DatafeedPreview } from '../datafeed_preview_flyout'; import { useToastNotificationService } from '../../../../../../services/toast_notification_service'; +const { collapseLiteralStrings } = XJson; + export enum EDITOR_MODE { HIDDEN, READONLY, diff --git a/x-pack/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/plugins/ml/public/application/util/dependency_cache.ts index 3680f8b63b0c96..0b78fb460ed859 100644 --- a/x-pack/plugins/ml/public/application/util/dependency_cache.ts +++ b/x-pack/plugins/ml/public/application/util/dependency_cache.ts @@ -24,7 +24,7 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/ import type { DashboardStart } from '@kbn/dashboard-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { DataViewsContract } from '@kbn/data-views-plugin/public'; -import type { SecurityPluginSetup } from '@kbn/security-plugin/public'; +import type { SecurityPluginStart } from '@kbn/security-plugin/public'; import type { MapsStartApi } from '@kbn/maps-plugin/public'; import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/public'; @@ -43,7 +43,7 @@ export interface DependencyCache { savedObjectsClient: SavedObjectsClientContract | null; application: ApplicationStart | null; http: HttpStart | null; - security: SecurityPluginSetup | undefined | null; + security: SecurityPluginStart | undefined | null; i18n: I18nStart | null; dashboard: DashboardStart | null; maps: MapsStartApi | null; diff --git a/x-pack/plugins/ml/public/maps/util.ts b/x-pack/plugins/ml/public/maps/util.ts index 51acd123398a78..9685d378556a1b 100644 --- a/x-pack/plugins/ml/public/maps/util.ts +++ b/x-pack/plugins/ml/public/maps/util.ts @@ -9,7 +9,10 @@ import { FeatureCollection, Feature, Geometry } from 'geojson'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { htmlIdGenerator } from '@elastic/eui'; import { FIELD_ORIGIN, STYLE_TYPE, LayerDescriptor } from '@kbn/maps-plugin/common'; -import { ESSearchSourceDescriptor } from '@kbn/maps-plugin/common/descriptor_types'; +import { + ESSearchSourceDescriptor, + VectorStyleDescriptor, +} from '@kbn/maps-plugin/common/descriptor_types'; import type { SerializableRecord } from '@kbn/utility-types'; import { fromKueryExpression, luceneStringToDsl, toElasticsearchQuery } from '@kbn/es-query'; import { ESSearchResponse } from '@kbn/core/types/elasticsearch'; @@ -20,6 +23,7 @@ import { formatHumanReadableDateTimeSeconds } from '../../common/util/date_utils import type { MlApiServices } from '../application/services/ml_api_service'; import { MLAnomalyDoc } from '../../common/types/anomalies'; import { SEARCH_QUERY_LANGUAGE } from '../../common/constants/search'; +import { tabColor } from '../../common/util/group_color_utils'; import { getIndexPattern } from '../application/explorer/reducers/explorer_reducer/get_index_pattern'; import { AnomalySource } from './anomaly_source'; import { SourceIndexGeoFields } from '../application/explorer/explorer_utils'; @@ -119,9 +123,28 @@ export function getInitialSourceIndexFieldLayers(sourceIndexWithGeoFields: Sourc const { dataViewId, geoFields } = sourceIndexWithGeoFields[index]; geoFields.forEach((geoField) => { + const color = tabColor(geoField); + initialLayers.push({ id: htmlIdGenerator()(), - type: LAYER_TYPE.MVT_VECTOR, + type: LAYER_TYPE.GEOJSON_VECTOR, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { + color, + }, + }, + lineColor: { + type: 'STATIC', + options: { + color, + }, + }, + }, + } as unknown as VectorStyleDescriptor, sourceDescriptor: { id: htmlIdGenerator()(), type: SOURCE_TYPES.ES_SEARCH, diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 9d084708e6529b..3037d841803493 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -30,7 +30,7 @@ import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/publ import type { LicenseManagementUIPluginSetup } from '@kbn/license-management-plugin/public'; import type { LicensingPluginSetup } from '@kbn/licensing-plugin/public'; -import type { SecurityPluginSetup } from '@kbn/security-plugin/public'; +import type { SecurityPluginStart } from '@kbn/security-plugin/public'; import type { MapsStartApi, MapsSetupApi } from '@kbn/maps-plugin/public'; import { @@ -66,10 +66,10 @@ export interface MlStartDependencies { charts: ChartsPluginStart; lens?: LensPublicStart; cases?: CasesUiStart; + security: SecurityPluginStart; } export interface MlSetupDependencies { - security?: SecurityPluginSetup; maps?: MapsSetupApi; licensing: LicensingPluginSetup; management?: ManagementSetup; @@ -119,7 +119,7 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> { unifiedSearch: pluginsStart.unifiedSearch, dashboard: pluginsStart.dashboard, share: pluginsStart.share, - security: pluginsSetup.security, + security: pluginsStart.security, licensing: pluginsSetup.licensing, management: pluginsSetup.management, licenseManagement: pluginsSetup.licenseManagement, diff --git a/x-pack/plugins/ml/readme.md b/x-pack/plugins/ml/readme.md index a29e976f12c462..b1b3ee8775dc21 100644 --- a/x-pack/plugins/ml/readme.md +++ b/x-pack/plugins/ml/readme.md @@ -117,7 +117,9 @@ With PATH_TO_CONFIG and other options as follows. Group | PATH_TO_CONFIG ----- | -------------- - anomaly detection | `test/functional/apps/ml/anomaly_detection/config.ts` + anomaly detection jobs | `test/functional/apps/ml/anomaly_detection_jobs/config.ts` + anomaly detection result views | `test/functional/apps/ml/anomaly_detection_result_views/config.ts` + anomaly detection integrations | `test/functional/apps/ml/anomaly_detection_integrations/config.ts` data frame analytics | `test/functional/apps/ml/data_frame_analytics/config.ts` data visualizer | `test/functional/apps/ml/data_visualizer/config.ts` permissions | `test/functional/apps/ml/permissions/config.ts` diff --git a/x-pack/plugins/ml/shared_imports.ts b/x-pack/plugins/ml/shared_imports.ts deleted file mode 100644 index 25f78cce556118..00000000000000 --- a/x-pack/plugins/ml/shared_imports.ts +++ /dev/null @@ -1,17 +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. - */ - -// eslint-disable-next-line @kbn/imports/no_boundary_crossing -export { EuiCodeEditor } from '@kbn/es-ui-shared-plugin/public'; -export type { EuiCodeEditorProps } from '@kbn/es-ui-shared-plugin/public'; - -// eslint-disable-next-line @kbn/imports/no_boundary_crossing -import { XJson } from '@kbn/es-ui-shared-plugin/public'; -const { collapseLiteralStrings, expandLiteralStrings } = XJson; - -export { XJsonMode } from '@kbn/ace'; -export { collapseLiteralStrings, expandLiteralStrings }; diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json index 55651e2f811657..a99bc950ca4458 100644 --- a/x-pack/plugins/ml/tsconfig.json +++ b/x-pack/plugins/ml/tsconfig.json @@ -11,7 +11,6 @@ "public/**/*", "server/**/*", "__mocks__/**/*", - "shared_imports.ts", "../../../typings/**/*", // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 "public/**/*.json", diff --git a/x-pack/plugins/monitoring/server/lib/standalone_clusters/standalone_cluster_query_filter.ts b/x-pack/plugins/monitoring/server/lib/standalone_clusters/standalone_cluster_query_filter.ts index b8712704f11f9f..12d140b97e27ee 100644 --- a/x-pack/plugins/monitoring/server/lib/standalone_clusters/standalone_cluster_query_filter.ts +++ b/x-pack/plugins/monitoring/server/lib/standalone_clusters/standalone_cluster_query_filter.ts @@ -23,6 +23,11 @@ export const standaloneClusterFilter = { field: 'cluster_uuid', }, }, + { + exists: { + field: 'error', + }, + }, ], }, }, diff --git a/x-pack/plugins/observability/public/components/app/section/index.tsx b/x-pack/plugins/observability/public/components/app/section/index.tsx index dae85d07685aed..149e386085d20b 100644 --- a/x-pack/plugins/observability/public/components/app/section/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/index.tsx @@ -49,6 +49,7 @@ export function SectionContainer({ initialIsOpen={initialIsOpen} id={title} buttonContentClassName="accordion-button" + data-test-subj={`accordion-${title}`} buttonContent={ <> <EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}> diff --git a/x-pack/plugins/observability/public/config/paths.ts b/x-pack/plugins/observability/public/config/paths.ts index 7c523f9bb12c78..4c7d4e7c45b775 100644 --- a/x-pack/plugins/observability/public/config/paths.ts +++ b/x-pack/plugins/observability/public/config/paths.ts @@ -14,8 +14,10 @@ export const paths = { rules: RULES_PAGE_LINK, ruleDetails: (ruleId?: string | null) => ruleId ? `${RULES_PAGE_LINK}/${encodeURI(ruleId)}` : RULES_PAGE_LINK, - alertDetails: (alertId?: string | null) => - alertId ? `${ALERT_PAGE_LINK}/${encodeURI(alertId)}` : ALERT_PAGE_LINK, + alertDetails: (alertId?: string | null, ruleId?: string | null) => + alertId && ruleId + ? `${ALERT_PAGE_LINK}/rules/${encodeURI(ruleId)}/alerts/${encodeURI(alertId)}` + : ALERT_PAGE_LINK, }, management: { rules: '/app/management/insightsAndAlerting/triggersActions/rules', diff --git a/x-pack/plugins/observability/public/pages/alerts/components/observability_actions.tsx b/x-pack/plugins/observability/public/pages/alerts/components/observability_actions.tsx index eddead1fa90dea..de631f92ef586a 100644 --- a/x-pack/plugins/observability/public/pages/alerts/components/observability_actions.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/observability_actions.tsx @@ -79,8 +79,8 @@ export function ObservabilityActions({ : null; const alertId = alert.fields['kibana.alert.uuid'] ?? null; const linkToAlert = - pageId !== ALERT_DETAILS_PAGE_ID && alertId - ? http.basePath.prepend(paths.observability.alertDetails(alertId)) + pageId !== ALERT_DETAILS_PAGE_ID && alertId && ruleId + ? http.basePath.prepend(paths.observability.alertDetails(alertId, ruleId)) : null; const caseAttachments: CaseAttachmentsWithoutOwner = useMemo(() => { return ecsData?._id diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx index 6ec620f535db75..974164265b50b0 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx @@ -14,7 +14,6 @@ import useAsync from 'react-use/lib/useAsync'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { loadRuleAggregations } from '@kbn/triggers-actions-ui-plugin/public'; import { AlertConsumers, AlertStatus } from '@kbn/rule-data-utils'; -import { buildEsQuery } from './helpers'; import { AlertStatusFilterButton } from '../../../../../common/typings'; import { useGetUserCasesPermissions } from '../../../../hooks/use_get_user_cases_permissions'; import { observabilityFeatureId } from '../../../../../common'; @@ -23,6 +22,7 @@ import { useAlertIndexNames } from '../../../../hooks/use_alert_index_names'; import { useHasData } from '../../../../hooks/use_has_data'; import { usePluginContext } from '../../../../hooks/use_plugin_context'; import { getNoDataConfig } from '../../../../utils/no_data_config'; +import { buildEsQuery } from '../../../../utils/build_es_query'; import { LoadingObservability } from '../../../overview'; import { Provider, @@ -35,6 +35,7 @@ import { renderRuleStats } from '../../components/rule_stats'; import { ObservabilityAppServices } from '../../../../application/types'; import { ALERT_STATUS_REGEX, + ALERTS_PER_PAGE, ALERTS_TABLE_ID, BASE_ALERT_REGEX, NO_INDEX_PATTERNS, @@ -144,11 +145,6 @@ function AlertsPage() { ]; }, [indexNames]); - const timeRange = { - to: rangeTo, - from: rangeFrom, - }; - const onRefresh = () => { setRefreshNow(new Date().getTime()); }; @@ -264,9 +260,15 @@ function AlertsPage() { AlertConsumers.LOGS, AlertConsumers.UPTIME, ]} - query={buildEsQuery(timeRange, kuery)} + query={buildEsQuery( + { + to: rangeTo, + from: rangeFrom, + }, + kuery + )} showExpandToDetails={false} - pageSize={50} + pageSize={ALERTS_PER_PAGE} refreshNow={refreshNow} /> </CasesContext> diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/constants.ts b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/constants.ts index 8630c7850298b7..83b059b04f5e35 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/constants.ts +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/constants.ts @@ -9,7 +9,8 @@ import { DataViewBase } from '@kbn/es-query'; import { ALERT_STATUS } from '@kbn/rule-data-utils'; export const ALERTS_PAGE_ID = 'alerts-o11y'; -export const ALERTS_TABLE_ID = 'xpack.observability.alerts.table'; +export const ALERTS_PER_PAGE = 50; +export const ALERTS_TABLE_ID = 'xpack.observability.alerts.alert.table'; const regExpEscape = (str: string) => str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); export const NO_INDEX_PATTERNS: DataViewBase[] = []; diff --git a/x-pack/plugins/observability/public/pages/overview/containers/overview_page/constants.ts b/x-pack/plugins/observability/public/pages/overview/containers/overview_page/constants.ts index b9036ea4320af2..cca2bb765e7192 100644 --- a/x-pack/plugins/observability/public/pages/overview/containers/overview_page/constants.ts +++ b/x-pack/plugins/observability/public/pages/overview/containers/overview_page/constants.ts @@ -7,5 +7,6 @@ export const CAPABILITIES_KEYS = ['logs', 'infrastructure', 'apm', 'uptime']; +export const ALERTS_TABLE_ID = 'xpack.observability.overview.alert.table'; export const ALERT_TABLE_STATE_STORAGE_KEY = 'xpack.observability.overview.alert.tableState'; export const ALERTS_PER_PAGE = 10; diff --git a/x-pack/plugins/observability/public/pages/overview/containers/overview_page/overview_page.tsx b/x-pack/plugins/observability/public/pages/overview/containers/overview_page/overview_page.tsx index 5a1dfdaa302526..1f09cfc38cf4ca 100644 --- a/x-pack/plugins/observability/public/pages/overview/containers/overview_page/overview_page.tsx +++ b/x-pack/plugins/observability/public/pages/overview/containers/overview_page/overview_page.tsx @@ -4,12 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFlyout, + EuiFlyoutSize, EuiFlyoutBody, EuiFlyoutHeader, EuiHorizontalRule, @@ -21,7 +23,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { AlertConsumers } from '@kbn/rule-data-utils'; import React, { useMemo, useRef, useCallback, useState, useEffect } from 'react'; import { calculateBucketSize } from './helpers'; @@ -38,10 +40,9 @@ import { useBreadcrumbs } from '../../../../hooks/use_breadcrumbs'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { useHasData } from '../../../../hooks/use_has_data'; import { usePluginContext } from '../../../../hooks/use_plugin_context'; -import { useAlertIndexNames } from '../../../../hooks/use_alert_index_names'; +import { buildEsQuery } from '../../../../utils/build_es_query'; import { getNewsFeed } from '../../../../services/get_news_feed'; import { DataSections, LoadingObservability } from '../../components'; -import { AlertsTableTGrid } from '../../../alerts/containers/alerts_table_t_grid/alerts_table_t_grid'; import { SectionContainer } from '../../../../components/app/section'; import { ObservabilityAppServices } from '../../../../application/types'; import { useGetUserCasesPermissions } from '../../../../hooks/use_get_user_cases_permissions'; @@ -51,7 +52,7 @@ import { ObservabilityStatusProgress } from '../../../../components/app/observab import { ObservabilityStatus } from '../../../../components/app/observability_status'; import { useGuidedSetupProgress } from '../../../../hooks/use_guided_setup_progress'; import { useObservabilityTourContext } from '../../../../components/shared/tour'; -import { CAPABILITIES_KEYS, ALERT_TABLE_STATE_STORAGE_KEY, ALERTS_PER_PAGE } from './constants'; +import { CAPABILITIES_KEYS, ALERTS_PER_PAGE, ALERTS_TABLE_ID } from './constants'; export function OverviewPage() { const trackMetric = useUiTracker({ app: 'observability-overview' }); @@ -65,12 +66,13 @@ export function OverviewPage() { }, ]); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const [refreshNow, setRefreshNow] = useState<number>(); - const indexNames = useAlertIndexNames(); const { cases, http, application: { capabilities }, + triggersActionsUi: { alertsTableConfigurationRegistry, getAlertsStateTable: AlertsStateTable }, } = useKibana<ObservabilityAppServices>().services; const { ObservabilityPageTemplate } = usePluginContext(); @@ -94,10 +96,6 @@ export function OverviewPage() { [absoluteStart, absoluteEnd] ); - const setRefetch = useCallback((ref) => { - refetch.current = ref; - }, []); - const handleGuidedSetupClick = useCallback(() => { if (isGuidedSetupProgressDismissed) { trackMetric({ metric: 'guided_setup_view_details_after_dismiss' }); @@ -107,6 +105,7 @@ export function OverviewPage() { }, [trackMetric, isGuidedSetupProgressDismissed, hideGuidedSetupTour]); const onTimeRangeRefresh = useCallback(() => { + setRefreshNow(new Date().getTime()); return refetch.current && refetch.current(); }, []); @@ -173,14 +172,24 @@ export function OverviewPage() { permissions={userCasesPermissions} features={{ alerts: { sync: false } }} > - <AlertsTableTGrid - setRefetch={setRefetch} - rangeFrom={relativeStart} - rangeTo={relativeEnd} - indexNames={indexNames} - itemsPerPage={ALERTS_PER_PAGE} - stateStorageKey={ALERT_TABLE_STATE_STORAGE_KEY} - storage={new Storage(window.localStorage)} + <AlertsStateTable + alertsTableConfigurationRegistry={alertsTableConfigurationRegistry} + configurationId={AlertConsumers.OBSERVABILITY} + id={ALERTS_TABLE_ID} + flyoutSize={'s' as EuiFlyoutSize} + featureIds={[ + AlertConsumers.APM, + AlertConsumers.INFRASTRUCTURE, + AlertConsumers.LOGS, + AlertConsumers.UPTIME, + ]} + query={buildEsQuery({ + from: relativeStart, + to: relativeEnd, + })} + showExpandToDetails={false} + pageSize={ALERTS_PER_PAGE} + refreshNow={refreshNow} /> </CasesContext> </SectionContainer> diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index 2f7ec525f46039..130a77f480da9f 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -117,7 +117,7 @@ export const routes = { params: {}, exact: true, }, - '/alerts/:alertId': { + '/alerts/rules/:ruleId/alerts/:alertId': { handler: () => { return <AlertDetailsPage />; }, diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/helpers/__snapshots__/build_es_query.test.ts.snap b/x-pack/plugins/observability/public/utils/build_es_query/__snapshots__/build_es_query.test.ts.snap similarity index 88% rename from x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/helpers/__snapshots__/build_es_query.test.ts.snap rename to x-pack/plugins/observability/public/utils/build_es_query/__snapshots__/build_es_query.test.ts.snap index f52b61794ce516..fcadce3f18b19c 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/helpers/__snapshots__/build_es_query.test.ts.snap +++ b/x-pack/plugins/observability/public/utils/build_es_query/__snapshots__/build_es_query.test.ts.snap @@ -145,3 +145,24 @@ Object { }, } `; + +exports[`buildEsQuery should generate correct es query for {"timeRange":{"from":"2022-08-30T15:23:23.721Z","to":"2022-08-30T15:38:28.171Z"}} 1`] = ` +Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "strict_date_optional_time", + "gte": "2022-08-30T15:23:23.721Z", + "lte": "2022-08-30T15:38:28.171Z", + }, + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, +} +`; diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/helpers/build_es_query.test.ts b/x-pack/plugins/observability/public/utils/build_es_query/build_es_query.test.ts similarity index 95% rename from x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/helpers/build_es_query.test.ts rename to x-pack/plugins/observability/public/utils/build_es_query/build_es_query.test.ts index ad303966d046b5..4bbacaa7bb1ad3 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/helpers/build_es_query.test.ts +++ b/x-pack/plugins/observability/public/utils/build_es_query/build_es_query.test.ts @@ -19,6 +19,9 @@ describe('buildEsQuery', () => { timeRange: defaultTimeRange, kuery: '', }, + { + timeRange: defaultTimeRange, + }, { timeRange: defaultTimeRange, kuery: 'nestedField: { child: "something" }', diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/helpers/build_es_query.ts b/x-pack/plugins/observability/public/utils/build_es_query/build_es_query.ts similarity index 74% rename from x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/helpers/build_es_query.ts rename to x-pack/plugins/observability/public/utils/build_es_query/build_es_query.ts index a6acbb0d2e40e2..28e2942c1f6069 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/helpers/build_es_query.ts +++ b/x-pack/plugins/observability/public/utils/build_es_query/build_es_query.ts @@ -9,13 +9,14 @@ import { buildEsQuery as kbnBuildEsQuery, TimeRange } from '@kbn/es-query'; import { TIMESTAMP } from '@kbn/rule-data-utils'; import { getTime } from '@kbn/data-plugin/common'; -export function buildEsQuery(timeRange: TimeRange, kuery: string) { +export function buildEsQuery(timeRange: TimeRange, kuery?: string) { const timeFilter = timeRange && getTime(undefined, timeRange, { fieldName: TIMESTAMP, }); const filtersToUse = [...(timeFilter ? [timeFilter] : [])]; + const queryToUse = kuery ? { query: kuery, language: 'kuery' } : []; - return kbnBuildEsQuery(undefined, { query: kuery, language: 'kuery' }, filtersToUse); + return kbnBuildEsQuery(undefined, queryToUse, filtersToUse); } diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/helpers/index.ts b/x-pack/plugins/observability/public/utils/build_es_query/index.ts similarity index 100% rename from x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/helpers/index.ts rename to x-pack/plugins/observability/public/utils/build_es_query/index.ts diff --git a/x-pack/plugins/observability/server/assets/constants.ts b/x-pack/plugins/observability/server/assets/constants.ts index 09d22022caffd0..8afa22d5f695ee 100644 --- a/x-pack/plugins/observability/server/assets/constants.ts +++ b/x-pack/plugins/observability/server/assets/constants.ts @@ -7,6 +7,9 @@ export const SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME = 'observability-slo-mappings'; export const SLO_COMPONENT_TEMPLATE_SETTINGS_NAME = 'observability-slo-settings'; -export const SLO_INDEX_TEMPLATE_NAME = 'observability-slo-data'; +export const SLO_INDEX_TEMPLATE_NAME = 'slo-observability.sli'; export const SLO_INGEST_PIPELINE_NAME = 'observability-slo-monthly-index'; export const SLO_RESOURCES_VERSION = 1; + +export const getSLODestinationIndexName = (spaceId: string) => + `${SLO_INDEX_TEMPLATE_NAME}-v${SLO_RESOURCES_VERSION}-${spaceId}`; diff --git a/x-pack/plugins/observability/server/assets/transform_templates/slo_transform_template.ts b/x-pack/plugins/observability/server/assets/transform_templates/slo_transform_template.ts new file mode 100644 index 00000000000000..6b313bdb76c5ab --- /dev/null +++ b/x-pack/plugins/observability/server/assets/transform_templates/slo_transform_template.ts @@ -0,0 +1,42 @@ +/* + * 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 { + TransformDestination, + TransformPivot, + TransformPutTransformRequest, + TransformSource, +} from '@elastic/elasticsearch/lib/api/types'; + +export const getSLOTransformTemplate = ( + transformId: string, + source: TransformSource, + destination: TransformDestination, + groupBy: TransformPivot['group_by'] = {}, + aggregations: TransformPivot['aggregations'] = {} +): TransformPutTransformRequest => ({ + transform_id: transformId, + source, + frequency: '1m', + dest: destination, + settings: { + deduce_mappings: false, + }, + sync: { + time: { + field: '@timestamp', + delay: '60s', + }, + }, + pivot: { + group_by: groupBy, + aggregations, + }, + _meta: { + version: 1, + }, +}); diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 5b47bbead83006..1d9d3cbf455f10 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -74,7 +74,7 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> { ui: casesCapabilities.all, }, read: { - api: ['bulkGetUserProfiles'], + api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles'], app: [casesFeatureId, 'kibana'], catalogue: [observabilityFeatureId], cases: { @@ -145,7 +145,6 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> { const start = () => core.getStartServices().then(([coreStart]) => coreStart); const { spacesService } = plugins.spaces; - const { ruleDataService } = plugins.ruleRegistry; registerRoutes({ diff --git a/x-pack/plugins/observability/server/routes/slo/route.ts b/x-pack/plugins/observability/server/routes/slo/route.ts index e868bc99a54175..c5b2e7d1030e62 100644 --- a/x-pack/plugins/observability/server/routes/slo/route.ts +++ b/x-pack/plugins/observability/server/routes/slo/route.ts @@ -5,6 +5,17 @@ * 2.0. */ +import uuid from 'uuid'; +import { + KibanaSavedObjectsSLORepository, + ResourceInstaller, + TransformInstaller, +} from '../../services/slo'; +import { + ApmTransactionDurationTransformGenerator, + ApmTransactionErrorRateTransformGenerator, +} from '../../services/slo/transform_generators'; +import { SLO } from '../../types/models'; import { createSLOParamsSchema } from '../../types/schema'; import { createObservabilityServerRoute } from '../create_observability_server_route'; @@ -14,8 +25,36 @@ const createSLORoute = createObservabilityServerRoute({ tags: [], }, params: createSLOParamsSchema, - handler: async ({ context, request, params }) => { - return { success: true }; + handler: async ({ context, request, params, logger, spacesService }) => { + const esClient = (await context.core).elasticsearch.client.asCurrentUser; + const soClient = (await context.core).savedObjects.client; + const spaceId = spacesService.getSpaceId(request); + + const resourceInstaller = new ResourceInstaller(esClient, logger); + const repository = new KibanaSavedObjectsSLORepository(soClient); + const transformInstaller = new TransformInstaller( + { + 'slo.apm.transaction_duration': new ApmTransactionDurationTransformGenerator(), + 'slo.apm.transaction_error_rate': new ApmTransactionErrorRateTransformGenerator(), + }, + esClient, + logger + ); + + await resourceInstaller.ensureCommonResourcesInstalled(spaceId); + + const slo: SLO = { + ...params.body, + id: uuid.v1(), + settings: { + destination_index: params.body.settings?.destination_index, + }, + }; + + await repository.save(slo); + await transformInstaller.installAndStartTransform(slo, spaceId); + + return slo; }, }); diff --git a/x-pack/plugins/observability/server/services/slo/fixtures/slo.ts b/x-pack/plugins/observability/server/services/slo/fixtures/slo.ts new file mode 100644 index 00000000000000..c6bdb2c5a1e77a --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/fixtures/slo.ts @@ -0,0 +1,51 @@ +/* + * 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 uuid from 'uuid'; +import { SLI, SLO } from '../../../types/models'; + +export const createSLO = (indicator: SLI): SLO => ({ + id: uuid.v1(), + name: 'irrelevant', + description: 'irrelevant', + indicator, + time_window: { + duration: '7d', + is_rolling: true, + }, + budgeting_method: 'occurrences', + objective: { + target: 0.999, + }, + settings: { + destination_index: 'some-index', + }, +}); + +export const createAPMTransactionErrorRateIndicator = (params = {}): SLI => ({ + type: 'slo.apm.transaction_error_rate', + params: { + environment: 'irrelevant', + service: 'irrelevant', + transaction_name: 'irrelevant', + transaction_type: 'irrelevant', + good_status_codes: ['2xx', '3xx', '4xx'], + ...params, + }, +}); + +export const createAPMTransactionDurationIndicator = (params = {}): SLI => ({ + type: 'slo.apm.transaction_duration', + params: { + environment: 'irrelevant', + service: 'irrelevant', + transaction_name: 'irrelevant', + transaction_type: 'irrelevant', + 'threshold.us': 500000, + ...params, + }, +}); diff --git a/x-pack/plugins/observability/server/services/slo/index.ts b/x-pack/plugins/observability/server/services/slo/index.ts index 39c288bbbf5393..d6b7d96fc112bd 100644 --- a/x-pack/plugins/observability/server/services/slo/index.ts +++ b/x-pack/plugins/observability/server/services/slo/index.ts @@ -7,3 +7,4 @@ export * from './resource_installer'; export * from './slo_repository'; +export * from './transform_installer'; diff --git a/x-pack/plugins/observability/server/services/slo/resource_installer.ts b/x-pack/plugins/observability/server/services/slo/resource_installer.ts index 92ea496e256df3..81b2a0e0eb4577 100644 --- a/x-pack/plugins/observability/server/services/slo/resource_installer.ts +++ b/x-pack/plugins/observability/server/services/slo/resource_installer.ts @@ -67,7 +67,9 @@ export class ResourceInstaller { } private getPipelinePrefix(version: number, spaceId: string): string { - return `${SLO_INDEX_TEMPLATE_NAME}-version-${version}-${spaceId}-`; + // Following https://www.elastic.co/blog/an-introduction-to-the-elastic-data-stream-naming-scheme + // slo-observability.sli-<version>-<namespace>.<index-date> + return `${SLO_INDEX_TEMPLATE_NAME}-v${version}-${spaceId}.`; } private async areResourcesAlreadyInstalled(): Promise<boolean> { diff --git a/x-pack/plugins/observability/server/services/slo/slo_repository.test.ts b/x-pack/plugins/observability/server/services/slo/slo_repository.test.ts index 8e7b7bbcac4277..265cc355860d98 100644 --- a/x-pack/plugins/observability/server/services/slo/slo_repository.test.ts +++ b/x-pack/plugins/observability/server/services/slo/slo_repository.test.ts @@ -5,7 +5,6 @@ * 2.0. */ -import uuid from 'uuid'; import { SavedObject } from '@kbn/core-saved-objects-common'; import { SavedObjectsClientContract } from '@kbn/core/server'; import { savedObjectsClientMock } from '@kbn/core/server/mocks'; @@ -13,33 +12,18 @@ import { savedObjectsClientMock } from '@kbn/core/server/mocks'; import { SLO, StoredSLO } from '../../types/models'; import { SO_SLO_TYPE } from '../../saved_objects'; import { KibanaSavedObjectsSLORepository } from './slo_repository'; +import { createSLO } from './fixtures/slo'; -const anSLO: SLO = { - id: uuid.v1(), - name: 'irrelevant', - description: 'irrelevant', - indicator: { - type: 'slo.apm.transaction_duration', - params: { - environment: 'irrelevant', - service: 'irrelevant', - transaction_type: 'irrelevant', - transaction_name: 'irrelevant', - 'threshold.us': 200000, - }, - }, - time_window: { - duration: '7d', - is_rolling: true, - }, - budgeting_method: 'occurrences', - objective: { - target: 0.999, +const anSLO = createSLO({ + type: 'slo.apm.transaction_duration', + params: { + environment: 'irrelevant', + service: 'irrelevant', + transaction_type: 'irrelevant', + transaction_name: 'irrelevant', + 'threshold.us': 200000, }, - settings: { - destination_index: 'some-index', - }, -}; +}); function aStoredSLO(slo: SLO): SavedObject<StoredSLO> { return { diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap new file mode 100644 index 00000000000000..ade6f8b90d8949 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap @@ -0,0 +1,139 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`APM Transaction Duration Transform Generator does not include the query filter when params are 'ALL' 1`] = ` +Object { + "bool": Object { + "filter": Array [ + Object { + "match": Object { + "transaction.root": true, + }, + }, + ], + }, +} +`; + +exports[`APM Transaction Duration Transform Generator returns the correct transform params with every specified indicator params 1`] = ` +Object { + "_meta": Object { + "version": 1, + }, + "dest": Object { + "index": "some-index", + }, + "frequency": "1m", + "pivot": Object { + "aggregations": Object { + "_numerator": Object { + "range": Object { + "field": "transaction.duration.histogram", + "ranges": Array [ + Object { + "to": 500000, + }, + ], + }, + }, + "slo.denominator": Object { + "value_count": Object { + "field": "transaction.duration.histogram", + }, + }, + "slo.numerator": Object { + "bucket_script": Object { + "buckets_path": Object { + "numerator": "_numerator['*-500000.0']>_count", + }, + "script": "params.numerator", + }, + }, + }, + "group_by": Object { + "@timestamp": Object { + "date_histogram": Object { + "calendar_interval": "1m", + "field": "@timestamp", + }, + }, + "slo.context.service.environment": Object { + "terms": Object { + "field": "service.environment", + }, + }, + "slo.context.service.name": Object { + "terms": Object { + "field": "service.name", + }, + }, + "slo.context.transaction.name": Object { + "terms": Object { + "field": "transaction.name", + }, + }, + "slo.context.transaction.type": Object { + "terms": Object { + "field": "transaction.type", + }, + }, + "slo.id": Object { + "terms": Object { + "field": "slo.id", + }, + }, + }, + }, + "settings": Object { + "deduce_mappings": false, + }, + "source": Object { + "index": "metrics-apm*", + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "match": Object { + "transaction.root": true, + }, + }, + Object { + "match": Object { + "service.name": "irrelevant", + }, + }, + Object { + "match": Object { + "service.environment": "irrelevant", + }, + }, + Object { + "match": Object { + "transaction.name": "irrelevant", + }, + }, + Object { + "match": Object { + "transaction.type": "irrelevant", + }, + }, + ], + }, + }, + "runtime_mappings": Object { + "slo.id": Object { + "script": Object { + "source": Any<String>, + }, + "type": "keyword", + }, + }, + }, + "sync": Object { + "time": Object { + "delay": "60s", + "field": "@timestamp", + }, + }, + "transform_id": Any<String>, +} +`; diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap new file mode 100644 index 00000000000000..d07a06e0724cf3 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap @@ -0,0 +1,177 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`APM Transaction Error Rate Transform Generator does not include the query filter when params are 'ALL' 1`] = ` +Object { + "bool": Object { + "filter": Array [ + Object { + "match": Object { + "transaction.root": true, + }, + }, + ], + }, +} +`; + +exports[`APM Transaction Error Rate Transform Generator returns the correct transform params with every specified indicator params 1`] = ` +Object { + "_meta": Object { + "version": 1, + }, + "dest": Object { + "index": "some-index", + }, + "frequency": "1m", + "pivot": Object { + "aggregations": Object { + "slo.denominator": Object { + "value_count": Object { + "field": "transaction.duration.histogram", + }, + }, + "slo.numerator": Object { + "filter": Object { + "bool": Object { + "should": Array [ + Object { + "match": Object { + "transaction.result": "HTTP 2xx", + }, + }, + Object { + "match": Object { + "transaction.result": "HTTP 3xx", + }, + }, + Object { + "match": Object { + "transaction.result": "HTTP 4xx", + }, + }, + ], + }, + }, + }, + }, + "group_by": Object { + "@timestamp": Object { + "date_histogram": Object { + "calendar_interval": "1m", + "field": "@timestamp", + }, + }, + "slo.context.service.environment": Object { + "terms": Object { + "field": "service.environment", + }, + }, + "slo.context.service.name": Object { + "terms": Object { + "field": "service.name", + }, + }, + "slo.context.transaction.name": Object { + "terms": Object { + "field": "transaction.name", + }, + }, + "slo.context.transaction.type": Object { + "terms": Object { + "field": "transaction.type", + }, + }, + "slo.id": Object { + "terms": Object { + "field": "slo.id", + }, + }, + }, + }, + "settings": Object { + "deduce_mappings": false, + }, + "source": Object { + "index": "metrics-apm*", + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "match": Object { + "transaction.root": true, + }, + }, + Object { + "match": Object { + "service.name": "irrelevant", + }, + }, + Object { + "match": Object { + "service.environment": "irrelevant", + }, + }, + Object { + "match": Object { + "transaction.name": "irrelevant", + }, + }, + Object { + "match": Object { + "transaction.type": "irrelevant", + }, + }, + ], + }, + }, + "runtime_mappings": Object { + "slo.id": Object { + "script": Object { + "source": Any<String>, + }, + "type": "keyword", + }, + }, + }, + "sync": Object { + "time": Object { + "delay": "60s", + "field": "@timestamp", + }, + }, + "transform_id": Any<String>, +} +`; + +exports[`APM Transaction Error Rate Transform Generator uses default values when 'good_status_codes' is not specified 1`] = ` +Object { + "slo.denominator": Object { + "value_count": Object { + "field": "transaction.duration.histogram", + }, + }, + "slo.numerator": Object { + "filter": Object { + "bool": Object { + "should": Array [ + Object { + "match": Object { + "transaction.result": "HTTP 2xx", + }, + }, + Object { + "match": Object { + "transaction.result": "HTTP 3xx", + }, + }, + Object { + "match": Object { + "transaction.result": "HTTP 4xx", + }, + }, + ], + }, + }, + }, +} +`; diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.test.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.test.ts new file mode 100644 index 00000000000000..1671e11d4cf2a0 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createAPMTransactionDurationIndicator, createSLO } from '../fixtures/slo'; +import { ApmTransactionDurationTransformGenerator } from './apm_transaction_duration'; + +const generator = new ApmTransactionDurationTransformGenerator(); + +describe('APM Transaction Duration Transform Generator', () => { + it('returns the correct transform params with every specified indicator params', async () => { + const anSLO = createSLO(createAPMTransactionDurationIndicator()); + const transform = generator.getTransformParams(anSLO, 'my-namespace'); + + expect(transform).toMatchSnapshot({ + transform_id: expect.any(String), + source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } }, + }); + expect(transform.transform_id).toEqual(`slo-${anSLO.id}`); + expect(transform.source.runtime_mappings!['slo.id']).toMatchObject({ + script: { source: `emit('${anSLO.id}')` }, + }); + }); + + it("does not include the query filter when params are 'ALL'", async () => { + const anSLO = createSLO( + createAPMTransactionDurationIndicator({ + environment: 'ALL', + service: 'ALL', + transaction_name: 'ALL', + transaction_type: 'ALL', + }) + ); + const transform = generator.getTransformParams(anSLO, 'my-namespace'); + + expect(transform.source.query).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts new file mode 100644 index 00000000000000..c00ba8f69d8059 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.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 { + AggregationsCalendarInterval, + MappingRuntimeFieldType, + TransformPutTransformRequest, +} from '@elastic/elasticsearch/lib/api/types'; +import { getSLODestinationIndexName, SLO_INGEST_PIPELINE_NAME } from '../../../assets/constants'; +import { getSLOTransformTemplate } from '../../../assets/transform_templates/slo_transform_template'; +import { + SLO, + apmTransactionDurationSLOSchema, + APMTransactionDurationSLO, +} from '../../../types/models'; +import { ALL_VALUE } from '../../../types/schema'; +import { TransformGenerator } from '.'; + +const APM_SOURCE_INDEX = 'metrics-apm*'; + +export class ApmTransactionDurationTransformGenerator implements TransformGenerator { + public getTransformParams(slo: SLO, spaceId: string): TransformPutTransformRequest { + if (!apmTransactionDurationSLOSchema.is(slo)) { + throw new Error(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); + } + + return getSLOTransformTemplate( + this.buildTransformId(slo), + this.buildSource(slo), + this.buildDestination(slo, spaceId), + this.buildGroupBy(), + this.buildAggregations(slo) + ); + } + + private buildTransformId(slo: APMTransactionDurationSLO): string { + return `slo-${slo.id}`; + } + + private buildSource(slo: APMTransactionDurationSLO) { + const queryFilter = []; + if (slo.indicator.params.service !== ALL_VALUE) { + queryFilter.push({ + match: { + 'service.name': slo.indicator.params.service, + }, + }); + } + + if (slo.indicator.params.environment !== ALL_VALUE) { + queryFilter.push({ + match: { + 'service.environment': slo.indicator.params.environment, + }, + }); + } + + if (slo.indicator.params.transaction_name !== ALL_VALUE) { + queryFilter.push({ + match: { + 'transaction.name': slo.indicator.params.transaction_name, + }, + }); + } + + if (slo.indicator.params.transaction_type !== ALL_VALUE) { + queryFilter.push({ + match: { + 'transaction.type': slo.indicator.params.transaction_type, + }, + }); + } + + return { + index: APM_SOURCE_INDEX, + runtime_mappings: { + 'slo.id': { + type: 'keyword' as MappingRuntimeFieldType, + script: { + source: `emit('${slo.id}')`, + }, + }, + }, + query: { + bool: { + filter: [ + { + match: { + 'transaction.root': true, + }, + }, + ...queryFilter, + ], + }, + }, + }; + } + + private buildDestination(slo: APMTransactionDurationSLO, spaceId: string) { + if (slo.settings.destination_index === undefined) { + return { + pipeline: SLO_INGEST_PIPELINE_NAME, + index: getSLODestinationIndexName(spaceId), + }; + } + + return { index: slo.settings.destination_index }; + } + + private buildGroupBy() { + return { + 'slo.id': { + terms: { + field: 'slo.id', + }, + }, + '@timestamp': { + date_histogram: { + field: '@timestamp', + calendar_interval: '1m' as AggregationsCalendarInterval, + }, + }, + 'slo.context.transaction.name': { + terms: { + field: 'transaction.name', + }, + }, + 'slo.context.transaction.type': { + terms: { + field: 'transaction.type', + }, + }, + 'slo.context.service.name': { + terms: { + field: 'service.name', + }, + }, + 'slo.context.service.environment': { + terms: { + field: 'service.environment', + }, + }, + }; + } + + private buildAggregations(slo: APMTransactionDurationSLO) { + const truncatedThreshold = Math.trunc(slo.indicator.params['threshold.us']); + + return { + _numerator: { + range: { + field: 'transaction.duration.histogram', + ranges: [ + { + to: truncatedThreshold, + }, + ], + }, + }, + 'slo.numerator': { + bucket_script: { + buckets_path: { + numerator: `_numerator['*-${truncatedThreshold}.0']>_count`, + }, + script: 'params.numerator', + }, + }, + 'slo.denominator': { + value_count: { + field: 'transaction.duration.histogram', + }, + }, + }; + } +} diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.test.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.test.ts new file mode 100644 index 00000000000000..0e9fb14f85468a --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createAPMTransactionErrorRateIndicator, createSLO } from '../fixtures/slo'; +import { ApmTransactionErrorRateTransformGenerator } from './apm_transaction_error_rate'; + +const generator = new ApmTransactionErrorRateTransformGenerator(); + +describe('APM Transaction Error Rate Transform Generator', () => { + it('returns the correct transform params with every specified indicator params', async () => { + const anSLO = createSLO(createAPMTransactionErrorRateIndicator()); + const transform = generator.getTransformParams(anSLO, 'my-namespace'); + + expect(transform).toMatchSnapshot({ + transform_id: expect.any(String), + source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } }, + }); + expect(transform.transform_id).toEqual(`slo-${anSLO.id}`); + expect(transform.source.runtime_mappings!['slo.id']).toMatchObject({ + script: { source: `emit('${anSLO.id}')` }, + }); + }); + + it("uses default values when 'good_status_codes' is not specified", async () => { + const anSLO = createSLO(createAPMTransactionErrorRateIndicator({ good_status_codes: [] })); + const transform = generator.getTransformParams(anSLO, 'my-namespace'); + + expect(transform.pivot?.aggregations).toMatchSnapshot(); + }); + + it("does not include the query filter when params are 'ALL'", async () => { + const anSLO = createSLO( + createAPMTransactionErrorRateIndicator({ + environment: 'ALL', + service: 'ALL', + transaction_name: 'ALL', + transaction_type: 'ALL', + }) + ); + const transform = generator.getTransformParams(anSLO, 'my-namespace'); + + expect(transform.source.query).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts new file mode 100644 index 00000000000000..c66de8913b6ef4 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts @@ -0,0 +1,185 @@ +/* + * 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 { + AggregationsCalendarInterval, + MappingRuntimeFieldType, + TransformPutTransformRequest, +} from '@elastic/elasticsearch/lib/api/types'; +import { getSLODestinationIndexName, SLO_INGEST_PIPELINE_NAME } from '../../../assets/constants'; +import { getSLOTransformTemplate } from '../../../assets/transform_templates/slo_transform_template'; +import { + apmTransactionErrorRateSLOSchema, + APMTransactionErrorRateSLO, + SLO, +} from '../../../types/models'; +import { ALL_VALUE } from '../../../types/schema'; +import { TransformGenerator } from '.'; + +const APM_SOURCE_INDEX = 'metrics-apm*'; +const ALLOWED_STATUS_CODES = ['2xx', '3xx', '4xx', '5xx']; +const DEFAULT_GOOD_STATUS_CODES = ['2xx', '3xx', '4xx']; + +export class ApmTransactionErrorRateTransformGenerator implements TransformGenerator { + public getTransformParams(slo: SLO, spaceId: string): TransformPutTransformRequest { + if (!apmTransactionErrorRateSLOSchema.is(slo)) { + throw new Error(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); + } + + return getSLOTransformTemplate( + this.buildTransformId(slo), + this.buildSource(slo), + this.buildDestination(slo, spaceId), + this.buildGroupBy(), + this.buildAggregations(slo) + ); + } + + private buildTransformId(slo: APMTransactionErrorRateSLO): string { + return `slo-${slo.id}`; + } + + private buildSource(slo: APMTransactionErrorRateSLO) { + const queryFilter = []; + if (slo.indicator.params.service !== ALL_VALUE) { + queryFilter.push({ + match: { + 'service.name': slo.indicator.params.service, + }, + }); + } + + if (slo.indicator.params.environment !== ALL_VALUE) { + queryFilter.push({ + match: { + 'service.environment': slo.indicator.params.environment, + }, + }); + } + + if (slo.indicator.params.transaction_name !== ALL_VALUE) { + queryFilter.push({ + match: { + 'transaction.name': slo.indicator.params.transaction_name, + }, + }); + } + + if (slo.indicator.params.transaction_type !== ALL_VALUE) { + queryFilter.push({ + match: { + 'transaction.type': slo.indicator.params.transaction_type, + }, + }); + } + + return { + index: APM_SOURCE_INDEX, + runtime_mappings: { + 'slo.id': { + type: 'keyword' as MappingRuntimeFieldType, + script: { + source: `emit('${slo.id}')`, + }, + }, + }, + query: { + bool: { + filter: [ + { + match: { + 'transaction.root': true, + }, + }, + ...queryFilter, + ], + }, + }, + }; + } + + private buildDestination(slo: APMTransactionErrorRateSLO, spaceId: string) { + if (slo.settings.destination_index === undefined) { + return { + pipeline: SLO_INGEST_PIPELINE_NAME, + index: getSLODestinationIndexName(spaceId), + }; + } + + return { index: slo.settings.destination_index }; + } + + private buildGroupBy() { + return { + 'slo.id': { + terms: { + field: 'slo.id', + }, + }, + '@timestamp': { + date_histogram: { + field: '@timestamp', + calendar_interval: '1m' as AggregationsCalendarInterval, + }, + }, + 'slo.context.transaction.name': { + terms: { + field: 'transaction.name', + }, + }, + 'slo.context.transaction.type': { + terms: { + field: 'transaction.type', + }, + }, + 'slo.context.service.name': { + terms: { + field: 'service.name', + }, + }, + 'slo.context.service.environment': { + terms: { + field: 'service.environment', + }, + }, + }; + } + + private buildAggregations(slo: APMTransactionErrorRateSLO) { + const goodStatusCodesFilter = this.getGoodStatusCodesFilter( + slo.indicator.params.good_status_codes + ); + + return { + 'slo.numerator': { + filter: { + bool: { + should: goodStatusCodesFilter, + }, + }, + }, + 'slo.denominator': { + value_count: { + field: 'transaction.duration.histogram', + }, + }, + }; + } + + private getGoodStatusCodesFilter(goodStatusCodes: string[] | undefined) { + let statusCodes = goodStatusCodes?.filter((code) => ALLOWED_STATUS_CODES.includes(code)); + if (statusCodes === undefined || statusCodes.length === 0) { + statusCodes = DEFAULT_GOOD_STATUS_CODES; + } + + return statusCodes.map((code) => ({ + match: { + 'transaction.result': `HTTP ${code}`, + }, + })); + } +} diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/index.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/index.ts new file mode 100644 index 00000000000000..6f0484c2044ada --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './transform_generator'; +export * from './apm_transaction_error_rate'; +export * from './apm_transaction_duration'; diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/transform_generator.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/transform_generator.ts new file mode 100644 index 00000000000000..21a917ea1af6d1 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/transform_generator.ts @@ -0,0 +1,13 @@ +/* + * 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { SLO } from '../../../types/models'; + +export interface TransformGenerator { + getTransformParams(slo: SLO, spaceId: string): TransformPutTransformRequest; +} diff --git a/x-pack/plugins/observability/server/services/slo/transform_installer.test.ts b/x-pack/plugins/observability/server/services/slo/transform_installer.test.ts new file mode 100644 index 00000000000000..cc65aac74c32eb --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/transform_installer.test.ts @@ -0,0 +1,102 @@ +/* + * 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. + */ +/* eslint-disable max-classes-per-file */ + +import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; +import { ElasticsearchClient } from '@kbn/core/server'; +import { MockedLogger } from '@kbn/logging-mocks'; +import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; + +import { TransformInstaller } from './transform_installer'; +import { + ApmTransactionErrorRateTransformGenerator, + TransformGenerator, +} from './transform_generators'; +import { SLO, SLITypes } from '../../types/models'; +import { createAPMTransactionErrorRateIndicator, createSLO } from './fixtures/slo'; + +describe('TransformerGenerator', () => { + let esClientMock: jest.Mocked<ElasticsearchClient>; + let loggerMock: jest.Mocked<MockedLogger>; + + beforeEach(() => { + esClientMock = elasticsearchServiceMock.createElasticsearchClient(); + loggerMock = loggingSystemMock.createLogger(); + }); + + describe('Unhappy path', () => { + it('throws when no generator exists for the slo indicator type', async () => { + // @ts-ignore defining only a subset of the possible SLI + const generators: Record<SLITypes, TransformGenerator> = { + 'slo.apm.transaction_duration': new DummyTransformGenerator(), + }; + const service = new TransformInstaller(generators, esClientMock, loggerMock); + + expect(() => + service.installAndStartTransform( + createSLO({ + type: 'slo.apm.transaction_error_rate', + params: { + environment: 'irrelevant', + service: 'irrelevant', + transaction_name: 'irrelevant', + transaction_type: 'irrelevant', + }, + }) + ) + ).rejects.toThrowError('Unsupported SLO type: slo.apm.transaction_error_rate'); + }); + + it('throws when transform generator fails', async () => { + // @ts-ignore defining only a subset of the possible SLI + const generators: Record<SLITypes, TransformGenerator> = { + 'slo.apm.transaction_duration': new FailTransformGenerator(), + }; + const service = new TransformInstaller(generators, esClientMock, loggerMock); + + expect(() => + service.installAndStartTransform( + createSLO({ + type: 'slo.apm.transaction_duration', + params: { + environment: 'irrelevant', + service: 'irrelevant', + transaction_name: 'irrelevant', + transaction_type: 'irrelevant', + 'threshold.us': 250000, + }, + }) + ) + ).rejects.toThrowError('Some error'); + }); + }); + + it('installs and starts the transform', async () => { + // @ts-ignore defining only a subset of the possible SLI + const generators: Record<SLITypes, TransformGenerator> = { + 'slo.apm.transaction_error_rate': new ApmTransactionErrorRateTransformGenerator(), + }; + const service = new TransformInstaller(generators, esClientMock, loggerMock); + + await service.installAndStartTransform(createSLO(createAPMTransactionErrorRateIndicator())); + + expect(esClientMock.transform.putTransform).toHaveBeenCalledTimes(1); + expect(esClientMock.transform.startTransform).toHaveBeenCalledTimes(1); + }); +}); + +class DummyTransformGenerator implements TransformGenerator { + getTransformParams(slo: SLO): TransformPutTransformRequest { + return {} as TransformPutTransformRequest; + } +} + +class FailTransformGenerator implements TransformGenerator { + getTransformParams(slo: SLO): TransformPutTransformRequest { + throw new Error('Some error'); + } +} diff --git a/x-pack/plugins/observability/server/services/slo/transform_installer.ts b/x-pack/plugins/observability/server/services/slo/transform_installer.ts new file mode 100644 index 00000000000000..cd677e10491ca5 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/transform_installer.ts @@ -0,0 +1,52 @@ +/* + * 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 { errors } from '@elastic/elasticsearch'; + +import { ElasticsearchClient, Logger } from '@kbn/core/server'; +import { SLO, SLITypes } from '../../types/models'; +import { TransformGenerator } from './transform_generators'; + +export class TransformInstaller { + constructor( + private generators: Record<SLITypes, TransformGenerator>, + private esClient: ElasticsearchClient, + private logger: Logger + ) {} + + async installAndStartTransform(slo: SLO, spaceId: string = 'default'): Promise<void> { + const generator = this.generators[slo.indicator.type]; + if (!generator) { + this.logger.error(`No transform generator found for ${slo.indicator.type} SLO type`); + throw new Error(`Unsupported SLO type: ${slo.indicator.type}`); + } + + const transformParams = generator.getTransformParams(slo, spaceId); + try { + await this.esClient.transform.putTransform(transformParams); + } catch (err) { + // swallow the error if the transform already exists. + const isAlreadyExistError = + err instanceof errors.ResponseError && + err?.body?.error?.type === 'resource_already_exists_exception'; + if (!isAlreadyExistError) { + this.logger.error(`Cannot create transform for ${slo.indicator.type} SLO type: ${err}`); + throw err; + } + } + + try { + await this.esClient.transform.startTransform( + { transform_id: transformParams.transform_id }, + { ignore: [409] } + ); + } catch (err) { + this.logger.error(`Cannot start transform id ${transformParams.transform_id}: ${err}`); + throw err; + } + } +} diff --git a/x-pack/plugins/observability/server/types/models/slo.ts b/x-pack/plugins/observability/server/types/models/slo.ts index 94017b50eb65a1..0cbb60531cc362 100644 --- a/x-pack/plugins/observability/server/types/models/slo.ts +++ b/x-pack/plugins/observability/server/types/models/slo.ts @@ -7,14 +7,20 @@ import * as t from 'io-ts'; -import { indicatorSchema, rollingTimeWindowSchema } from '../schema'; +import { + apmTransactionDurationIndicatorSchema, + apmTransactionErrorRateIndicatorSchema, + indicatorSchema, + indicatorTypesSchema, + rollingTimeWindowSchema, +} from '../schema'; const baseSLOSchema = t.type({ id: t.string, name: t.string, description: t.string, - indicator: indicatorSchema, time_window: rollingTimeWindowSchema, + indicator: indicatorSchema, budgeting_method: t.literal('occurrences'), objective: t.type({ target: t.number, @@ -24,10 +30,26 @@ const baseSLOSchema = t.type({ }), }); +export const apmTransactionErrorRateSLOSchema = t.intersection([ + baseSLOSchema, + t.type({ indicator: apmTransactionErrorRateIndicatorSchema }), +]); + +export const apmTransactionDurationSLOSchema = t.intersection([ + baseSLOSchema, + t.type({ indicator: apmTransactionDurationIndicatorSchema }), +]); + const storedSLOSchema = t.intersection([ baseSLOSchema, t.type({ created_at: t.string, updated_at: t.string }), ]); export type SLO = t.TypeOf<typeof baseSLOSchema>; +export type APMTransactionErrorRateSLO = t.TypeOf<typeof apmTransactionErrorRateSLOSchema>; +export type APMTransactionDurationSLO = t.TypeOf<typeof apmTransactionDurationSLOSchema>; + +export type SLI = t.TypeOf<typeof indicatorSchema>; +export type SLITypes = t.TypeOf<typeof indicatorTypesSchema>; + export type StoredSLO = t.TypeOf<typeof storedSLOSchema>; diff --git a/x-pack/plugins/observability/server/types/schema/slo.ts b/x-pack/plugins/observability/server/types/schema/slo.ts index 62495ff26d4f49..2896e443e2c37c 100644 --- a/x-pack/plugins/observability/server/types/schema/slo.ts +++ b/x-pack/plugins/observability/server/types/schema/slo.ts @@ -7,10 +7,12 @@ import * as t from 'io-ts'; -const allOrAnyString = t.union([t.literal('ALL'), t.string]); +export const ALL_VALUE = 'ALL'; +const allOrAnyString = t.union([t.literal(ALL_VALUE), t.string]); -const apmTransactionDurationIndicatorSchema = t.type({ - type: t.literal('slo.apm.transaction_duration'), +const apmTransactionDurationIndicatorTypeSchema = t.literal('slo.apm.transaction_duration'); +export const apmTransactionDurationIndicatorSchema = t.type({ + type: apmTransactionDurationIndicatorTypeSchema, params: t.type({ environment: allOrAnyString, service: allOrAnyString, @@ -20,8 +22,9 @@ const apmTransactionDurationIndicatorSchema = t.type({ }), }); -const apmTransactionErrorRateIndicatorSchema = t.type({ - type: t.literal('slo.apm.transaction_error_rate'), +const apmTransactionErrorRateIndicatorTypeSchema = t.literal('slo.apm.transaction_error_rate'); +export const apmTransactionErrorRateIndicatorSchema = t.type({ + type: apmTransactionErrorRateIndicatorTypeSchema, params: t.intersection([ t.type({ environment: allOrAnyString, @@ -42,6 +45,11 @@ export const rollingTimeWindowSchema = t.type({ is_rolling: t.literal(true), }); +export const indicatorTypesSchema = t.union([ + apmTransactionDurationIndicatorTypeSchema, + apmTransactionErrorRateIndicatorTypeSchema, +]); + export const indicatorSchema = t.union([ apmTransactionDurationIndicatorSchema, apmTransactionErrorRateIndicatorSchema, diff --git a/x-pack/plugins/osquery/cypress.config.ts b/x-pack/plugins/osquery/cypress.config.ts new file mode 100644 index 00000000000000..862b4a916beada --- /dev/null +++ b/x-pack/plugins/osquery/cypress.config.ts @@ -0,0 +1,42 @@ +/* + * 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. + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import { defineConfig } from 'cypress'; + +// eslint-disable-next-line import/no-default-export +export default defineConfig({ + defaultCommandTimeout: 60000, + execTimeout: 120000, + pageLoadTimeout: 12000, + + retries: { + runMode: 1, + openMode: 0, + }, + + screenshotsFolder: '../../../target/kibana-osquery/cypress/screenshots', + trashAssetsBeforeRuns: false, + video: false, + videosFolder: '../../../target/kibana-osquery/cypress/videos', + viewportHeight: 900, + viewportWidth: 1440, + experimentalStudio: true, + + env: { + 'cypress-react-selector': { + root: '#osquery-app', + }, + }, + + e2e: { + baseUrl: 'http://localhost:5601', + setupNodeEvents(on, config) { + // implement node event listeners here + }, + }, +}); diff --git a/x-pack/plugins/osquery/cypress/cypress.json b/x-pack/plugins/osquery/cypress/cypress.json deleted file mode 100644 index 5df26a922d7c3b..00000000000000 --- a/x-pack/plugins/osquery/cypress/cypress.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "baseUrl": "http://localhost:5620", - "defaultCommandTimeout": 60000, - "execTimeout": 120000, - "pageLoadTimeout": 12000, - "retries": { - "runMode": 1, - "openMode": 0 - }, - "screenshotsFolder": "../../../target/kibana-osquery/cypress/screenshots", - "trashAssetsBeforeRuns": false, - "video": false, - "videosFolder": "../../../target/kibana-osquery/cypress/videos", - "viewportHeight": 900, - "viewportWidth": 1440, - "experimentalStudio": true, - "env": { - "cypress-react-selector": { - "root": "#osquery-app" - } - } -} diff --git a/x-pack/plugins/osquery/cypress/integration/all/add_integration.spec.ts b/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts similarity index 100% rename from x-pack/plugins/osquery/cypress/integration/all/add_integration.spec.ts rename to x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts diff --git a/x-pack/plugins/osquery/cypress/integration/all/alerts.spec.ts b/x-pack/plugins/osquery/cypress/e2e/all/alerts.cy.ts similarity index 100% rename from x-pack/plugins/osquery/cypress/integration/all/alerts.spec.ts rename to x-pack/plugins/osquery/cypress/e2e/all/alerts.cy.ts diff --git a/x-pack/plugins/osquery/cypress/integration/all/discover.spec.ts b/x-pack/plugins/osquery/cypress/e2e/all/discover.cy.ts similarity index 100% rename from x-pack/plugins/osquery/cypress/integration/all/discover.spec.ts rename to x-pack/plugins/osquery/cypress/e2e/all/discover.cy.ts diff --git a/x-pack/plugins/osquery/cypress/integration/all/edit_saved_queries.spec.ts b/x-pack/plugins/osquery/cypress/e2e/all/edit_saved_queries.cy.ts similarity index 100% rename from x-pack/plugins/osquery/cypress/integration/all/edit_saved_queries.spec.ts rename to x-pack/plugins/osquery/cypress/e2e/all/edit_saved_queries.cy.ts diff --git a/x-pack/plugins/osquery/cypress/integration/all/live_query.spec.ts b/x-pack/plugins/osquery/cypress/e2e/all/live_query.cy.ts similarity index 100% rename from x-pack/plugins/osquery/cypress/integration/all/live_query.spec.ts rename to x-pack/plugins/osquery/cypress/e2e/all/live_query.cy.ts diff --git a/x-pack/plugins/osquery/cypress/integration/all/metrics.spec.ts b/x-pack/plugins/osquery/cypress/e2e/all/metrics.cy.ts similarity index 100% rename from x-pack/plugins/osquery/cypress/integration/all/metrics.spec.ts rename to x-pack/plugins/osquery/cypress/e2e/all/metrics.cy.ts diff --git a/x-pack/plugins/osquery/cypress/integration/all/packs.spec.ts b/x-pack/plugins/osquery/cypress/e2e/all/packs.cy.ts similarity index 100% rename from x-pack/plugins/osquery/cypress/integration/all/packs.spec.ts rename to x-pack/plugins/osquery/cypress/e2e/all/packs.cy.ts diff --git a/x-pack/plugins/osquery/cypress/integration/all/saved_queries.spec.ts b/x-pack/plugins/osquery/cypress/e2e/all/saved_queries.cy.ts similarity index 100% rename from x-pack/plugins/osquery/cypress/integration/all/saved_queries.spec.ts rename to x-pack/plugins/osquery/cypress/e2e/all/saved_queries.cy.ts diff --git a/x-pack/plugins/osquery/cypress/integration/roles/admin.spec.ts b/x-pack/plugins/osquery/cypress/e2e/roles/admin.cy.ts similarity index 100% rename from x-pack/plugins/osquery/cypress/integration/roles/admin.spec.ts rename to x-pack/plugins/osquery/cypress/e2e/roles/admin.cy.ts diff --git a/x-pack/plugins/osquery/cypress/integration/roles/alert_test.spec.ts b/x-pack/plugins/osquery/cypress/e2e/roles/alert_test.cy.ts similarity index 100% rename from x-pack/plugins/osquery/cypress/integration/roles/alert_test.spec.ts rename to x-pack/plugins/osquery/cypress/e2e/roles/alert_test.cy.ts diff --git a/x-pack/plugins/osquery/cypress/integration/roles/reader.spec.ts b/x-pack/plugins/osquery/cypress/e2e/roles/reader.cy.ts similarity index 100% rename from x-pack/plugins/osquery/cypress/integration/roles/reader.spec.ts rename to x-pack/plugins/osquery/cypress/e2e/roles/reader.cy.ts diff --git a/x-pack/plugins/osquery/cypress/integration/roles/t1_analyst.spec.ts b/x-pack/plugins/osquery/cypress/e2e/roles/t1_analyst.cy.ts similarity index 100% rename from x-pack/plugins/osquery/cypress/integration/roles/t1_analyst.spec.ts rename to x-pack/plugins/osquery/cypress/e2e/roles/t1_analyst.cy.ts diff --git a/x-pack/plugins/osquery/cypress/integration/roles/t2_analyst.spec.ts b/x-pack/plugins/osquery/cypress/e2e/roles/t2_analyst.cy.ts similarity index 100% rename from x-pack/plugins/osquery/cypress/integration/roles/t2_analyst.spec.ts rename to x-pack/plugins/osquery/cypress/e2e/roles/t2_analyst.cy.ts diff --git a/x-pack/plugins/osquery/cypress/support/coverage.ts b/x-pack/plugins/osquery/cypress/support/coverage.ts index 65975f65af24fe..9278edfcc6ddd7 100644 --- a/x-pack/plugins/osquery/cypress/support/coverage.ts +++ b/x-pack/plugins/osquery/cypress/support/coverage.ts @@ -47,7 +47,8 @@ const logMessage = (s: string) => { * If there are more files loaded from support folder, also removes them */ const filterSupportFilesFromCoverage = (totalCoverage: any) => { - const integrationFolder = Cypress.config('integrationFolder'); + // @ts-expect-error update types + const integrationFolder = Cypress.config('e2eFolder'); const supportFile = Cypress.config('supportFile'); /** @type {string} Cypress run-time config has the support folder string */ @@ -64,6 +65,7 @@ const filterSupportFilesFromCoverage = (totalCoverage: any) => { // if we have files from support folder AND the support folder is not same // as the integration, or its prefix (this might remove all app source files) // then remove all files from the support folder + // @ts-expect-error update types if (!integrationFolder.startsWith(supportFolder)) { // remove all covered files from support folder coverage = Cypress._.omitBy(totalCoverage, (fileCoverage, filename) => diff --git a/x-pack/plugins/osquery/cypress/support/index.ts b/x-pack/plugins/osquery/cypress/support/e2e.ts similarity index 100% rename from x-pack/plugins/osquery/cypress/support/index.ts rename to x-pack/plugins/osquery/cypress/support/e2e.ts diff --git a/x-pack/plugins/osquery/cypress/tsconfig.json b/x-pack/plugins/osquery/cypress/tsconfig.json index cbb5b10c48aaf2..548ac5dc3eb130 100644 --- a/x-pack/plugins/osquery/cypress/tsconfig.json +++ b/x-pack/plugins/osquery/cypress/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "../../../../tsconfig.base.json", "include": [ - "**/*" + "**/*", + "../cypress.config.ts" ], "exclude": [ "target/**/*" diff --git a/x-pack/plugins/osquery/package.json b/x-pack/plugins/osquery/package.json index 8d0e928f727707..fdda0a2316779b 100644 --- a/x-pack/plugins/osquery/package.json +++ b/x-pack/plugins/osquery/package.json @@ -5,9 +5,9 @@ "private": true, "license": "Elastic-License", "scripts": { - "cypress:open": "../../../node_modules/.bin/cypress open --config-file ./cypress/cypress.json", + "cypress:open": "../../../node_modules/.bin/cypress open --config-file ./cypress.config.ts", "cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/osquery_cypress/visual_config.ts", - "cypress:run": "../../../node_modules/.bin/cypress run --config-file ./cypress/cypress.json", + "cypress:run": "../../../node_modules/.bin/cypress run --config-file ./cypress.config.ts", "cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/osquery_cypress/cli_config.ts", "nyc": "../../../node_modules/.bin/nyc report --reporter=text-summary" } diff --git a/x-pack/plugins/osquery/public/live_queries/form/index.tsx b/x-pack/plugins/osquery/public/live_queries/form/index.tsx index 3ed54c451f38b9..96afe9deb98e2e 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/index.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/index.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import type { EuiAccordionProps } from '@elastic/eui'; import { EuiFormRow } from '@elastic/eui'; import { EuiButton, @@ -13,16 +12,15 @@ import { EuiSpacer, EuiFlexGroup, EuiFlexItem, - EuiAccordion, EuiCard, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { useForm as useHookForm, FormProvider } from 'react-hook-form'; - import { isEmpty, map, find, pickBy } from 'lodash'; import { i18n } from '@kbn/i18n'; + import type { SavedQuerySOFormData } from '../../saved_queries/form/use_saved_query_form'; import type { EcsMappingFormField, @@ -33,8 +31,6 @@ import { convertECSMappingToObject } from '../../../common/schemas/common/utils' import { useKibana } from '../../common/lib/kibana'; import { ResultTabs } from '../../routes/saved_queries/edit/tabs'; import { SavedQueryFlyout } from '../../saved_queries'; -import { ECSMappingEditorField } from '../../packs/queries/lazy_ecs_mapping_editor_field'; -import { SavedQueriesDropdown } from '../../saved_queries/saved_queries_dropdown'; import { usePacks } from '../../packs/use_packs'; import { PackQueriesStatusTable } from './pack_queries_status_table'; import { useCreateLiveQuery } from '../use_create_live_query_action'; @@ -99,13 +95,6 @@ const StyledEuiCard = styled(EuiCard)` } `; -const StyledEuiAccordion = styled(EuiAccordion)` - ${({ isDisabled }: { isDisabled?: boolean }) => isDisabled && 'display: none;'} - .euiAccordion__button { - color: ${({ theme }) => theme.eui.euiColorPrimary}; - } -`; - type FormType = 'simple' | 'steps'; interface LiveQueryFormProps { @@ -123,7 +112,6 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({ defaultValue, onSuccess, queryField = true, - ecsMappingField = true, formType = 'steps', enabled = true, hideAgentsField = false, @@ -161,8 +149,6 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({ [permissions] ); - const [advancedContentState, setAdvancedContentState] = - useState<EuiAccordionProps['forceState']>('closed'); const [showSavedQueryFlyout, setShowSavedQueryFlyout] = useState(false); const [queryType, setQueryType] = useState<string>('query'); const [isLive, setIsLive] = useState(false); @@ -208,43 +194,14 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({ [queryStatus] ); - const handleSavedQueryChange = useCallback( - (savedQuery) => { - if (savedQuery) { - setValue('query', savedQuery.query); - setValue('savedQueryId', savedQuery.savedQueryId); - setValue( - 'ecs_mapping', - !isEmpty(savedQuery.ecs_mapping) - ? map(savedQuery.ecs_mapping, (value, key) => ({ - key, - result: { - type: Object.keys(value)[0], - value: Object.values(value)[0] as string, - }, - })) - : [defaultEcsFormData] - ); - - if (!isEmpty(savedQuery.ecs_mapping)) { - setAdvancedContentState('open'); - } - } else { - setValue('savedQueryId', null); - } - }, - [setValue] - ); - const onSubmit = useCallback( - // not sure why, but submitOnCmdEnter doesn't have proper form values so I am passing them in manually - async (values: LiveQueryFormFields = watchedValues) => { + async (values: LiveQueryFormFields) => { const serializedData = pickBy( { agentSelection: values.agentSelection, saved_query_id: values.savedQueryId, query: values.query, - pack_id: packId?.length ? packId[0] : undefined, + pack_id: values?.packId?.length ? values?.packId[0] : undefined, ...(values.ecs_mapping ? { ecs_mapping: convertECSMappingToObject(values.ecs_mapping) } : {}), @@ -259,25 +216,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({ } catch (e) {} } }, - [errors, mutateAsync, packId, watchedValues] - ); - const commands = useMemo( - () => [ - { - name: 'submitOnCmdEnter', - bindKey: { win: 'ctrl+enter', mac: 'cmd+enter' }, - // @ts-expect-error update types - explanation in onSubmit() - exec: () => handleSubmit(onSubmit)(watchedValues), - }, - ], - [handleSubmit, onSubmit, watchedValues] - ); - - const queryComponentProps = useMemo( - () => ({ - commands, - }), - [commands] + [errors, mutateAsync] ); const serializedData: SavedQuerySOFormData = useMemo( @@ -285,23 +224,6 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({ [watchedValues] ); - const handleToggle = useCallback((isOpen) => { - const newState = isOpen ? 'open' : 'closed'; - setAdvancedContentState(newState); - }, []); - - const ecsFieldProps = useMemo( - () => ({ - isDisabled: !permissions.writeLiveQueries, - }), - [permissions.writeLiveQueries] - ); - - const isSavedQueryDisabled = useMemo( - () => !permissions.runSavedQueries || !permissions.readSavedQueries, - [permissions.readSavedQueries, permissions.runSavedQueries] - ); - const { data: packsData, isFetched: isPackDataFetched } = usePacks({}); const selectedPackData = useMemo( @@ -309,6 +231,8 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({ [packId, packsData] ); + const handleSubmitForm = useMemo(() => handleSubmit(onSubmit), [handleSubmit, onSubmit]); + const submitButtonContent = useMemo( () => ( <EuiFlexItem> @@ -330,7 +254,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({ <EuiButton id="submit-button" disabled={!enabled || isSubmitting} - onClick={handleSubmit(onSubmit)} + onClick={handleSubmitForm} > <FormattedMessage id="xpack.osquery.liveQueryForm.form.submitButtonLabel" @@ -349,53 +273,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({ handleShowSaveQueryFlyout, enabled, isSubmitting, - handleSubmit, - onSubmit, - ] - ); - - const queryFieldStepContent = useMemo( - () => ( - <> - {queryField && ( - <> - {!isSavedQueryDisabled && ( - <> - <SavedQueriesDropdown - disabled={isSavedQueryDisabled} - onChange={handleSavedQueryChange} - /> - </> - )} - <LiveQueryQueryField {...queryComponentProps} queryType={queryType} /> - </> - )} - {ecsMappingField && ( - <> - <EuiSpacer size="m" /> - <StyledEuiAccordion - id="advanced" - forceState={advancedContentState} - onToggle={handleToggle} - buttonContent="Advanced" - > - <EuiSpacer size="xs" /> - <ECSMappingEditorField euiFieldProps={ecsFieldProps} /> - </StyledEuiAccordion> - </> - )} - </> - ), - [ - queryField, - isSavedQueryDisabled, - handleSavedQueryChange, - queryComponentProps, - queryType, - ecsMappingField, - advancedContentState, - handleToggle, - ecsFieldProps, + handleSubmitForm, ] ); @@ -589,7 +467,9 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({ </> ) : ( <> - <EuiFlexItem>{queryFieldStepContent}</EuiFlexItem> + <EuiFlexItem> + <LiveQueryQueryField handleSubmitForm={handleSubmitForm} /> + </EuiFlexItem> {submitButtonContent} <EuiFlexItem>{resultsStepContent}</EuiFlexItem> </> diff --git a/x-pack/plugins/osquery/public/live_queries/form/live_query_query_field.tsx b/x-pack/plugins/osquery/public/live_queries/form/live_query_query_field.tsx index e3516f982cc0b3..2938251e177be2 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/live_query_query_field.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/live_query_query_field.tsx @@ -5,33 +5,45 @@ * 2.0. */ -import { EuiCodeBlock, EuiFormRow } from '@elastic/eui'; -import React from 'react'; +import { isEmpty, map } from 'lodash'; +import type { EuiAccordionProps } from '@elastic/eui'; +import { EuiCodeBlock, EuiFormRow, EuiAccordion, EuiSpacer } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; - -import { useController } from 'react-hook-form'; +import { useController, useFormContext } from 'react-hook-form'; import { i18n } from '@kbn/i18n'; -import type { EuiCodeEditorProps } from '../../shared_imports'; import { OsqueryEditor } from '../../editor'; import { useKibana } from '../../common/lib/kibana'; import { MAX_QUERY_LENGTH } from '../../packs/queries/validations'; +import { ECSMappingEditorField } from '../../packs/queries/lazy_ecs_mapping_editor_field'; +import type { SavedQueriesDropdownProps } from '../../saved_queries/saved_queries_dropdown'; +import { SavedQueriesDropdown } from '../../saved_queries/saved_queries_dropdown'; + +const StyledEuiAccordion = styled(EuiAccordion)` + ${({ isDisabled }: { isDisabled?: boolean }) => isDisabled && 'display: none;'} + .euiAccordion__button { + color: ${({ theme }) => theme.eui.euiColorPrimary}; + } +`; const StyledEuiCodeBlock = styled(EuiCodeBlock)` min-height: 100px; `; -interface LiveQueryQueryFieldProps { +export interface LiveQueryQueryFieldProps { disabled?: boolean; - commands?: EuiCodeEditorProps['commands']; - queryType: string; + handleSubmitForm?: () => void; } const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({ disabled, - commands, - queryType, + handleSubmitForm, }) => { + const formContext = useFormContext(); + const [advancedContentState, setAdvancedContentState] = + useState<EuiAccordionProps['forceState']>('closed'); const permissions = useKibana().services.application.capabilities.osquery; + const queryType = formContext?.watch('queryType', 'query'); const { field: { onChange, value }, @@ -43,7 +55,7 @@ const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({ message: i18n.translate('xpack.osquery.pack.queryFlyoutForm.emptyQueryError', { defaultMessage: 'Query is a required field', }), - value: queryType === 'query', + value: queryType !== 'pack', }, maxLength: { message: i18n.translate('xpack.osquery.liveQuery.queryForm.largeQueryError', { @@ -56,27 +68,108 @@ const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({ defaultValue: '', }); + const handleSavedQueryChange: SavedQueriesDropdownProps['onChange'] = useCallback( + (savedQuery) => { + if (savedQuery) { + formContext?.setValue('query', savedQuery.query); + formContext?.setValue('savedQueryId', savedQuery.savedQueryId); + if (!isEmpty(savedQuery.ecs_mapping)) { + formContext?.setValue( + 'ecs_mapping', + map(savedQuery.ecs_mapping, (ecsValue, key) => ({ + key, + result: { + type: Object.keys(ecsValue)[0], + value: Object.values(ecsValue)[0] as string, + }, + })) + ); + } else { + formContext?.resetField('ecs_mapping'); + } + + if (!isEmpty(savedQuery.ecs_mapping)) { + setAdvancedContentState('open'); + } + } else { + formContext?.setValue('savedQueryId', null); + } + }, + [formContext] + ); + + const handleToggle = useCallback((isOpen) => { + const newState = isOpen ? 'open' : 'closed'; + setAdvancedContentState(newState); + }, []); + + const ecsFieldProps = useMemo( + () => ({ + isDisabled: !permissions.writeLiveQueries, + }), + [permissions.writeLiveQueries] + ); + + const isSavedQueryDisabled = useMemo( + () => !permissions.runSavedQueries || !permissions.readSavedQueries, + [permissions.readSavedQueries, permissions.runSavedQueries] + ); + + const commands = useMemo( + () => + handleSubmitForm + ? [ + { + name: 'submitOnCmdEnter', + bindKey: { win: 'ctrl+enter', mac: 'cmd+enter' }, + exec: handleSubmitForm, + }, + ] + : [], + [handleSubmitForm] + ); + return ( - <EuiFormRow - isInvalid={!!error?.message} - error={error?.message} - fullWidth - isDisabled={!permissions.writeLiveQueries || disabled} - > - {!permissions.writeLiveQueries || disabled ? ( - <StyledEuiCodeBlock - language="sql" - fontSize="m" - paddingSize="m" - transparentBackground={!value.length} - > - {value} - </StyledEuiCodeBlock> - ) : ( - <OsqueryEditor defaultValue={value} onChange={onChange} commands={commands} /> + <> + {!isSavedQueryDisabled && ( + <SavedQueriesDropdown disabled={isSavedQueryDisabled} onChange={handleSavedQueryChange} /> )} - </EuiFormRow> + <EuiFormRow + isInvalid={!!error?.message} + error={error?.message} + fullWidth + isDisabled={!permissions.writeLiveQueries || disabled} + > + {!permissions.writeLiveQueries || disabled ? ( + <StyledEuiCodeBlock + language="sql" + fontSize="m" + paddingSize="m" + transparentBackground={!value.length} + > + {value} + </StyledEuiCodeBlock> + ) : ( + <OsqueryEditor defaultValue={value} onChange={onChange} commands={commands} /> + )} + </EuiFormRow> + + <EuiSpacer size="m" /> + + <StyledEuiAccordion + id="advanced" + forceState={advancedContentState} + onToggle={handleToggle} + buttonContent="Advanced" + > + <EuiSpacer size="xs" /> + <ECSMappingEditorField euiFieldProps={ecsFieldProps} /> + </StyledEuiAccordion> + </> ); }; export const LiveQueryQueryField = React.memo(LiveQueryQueryFieldComponent); + +// eslint-disable-next-line import/no-default-export +export { LiveQueryQueryField as default }; diff --git a/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx b/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx index 40eb009a71bd1e..7a67c6fdeb65b8 100644 --- a/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx +++ b/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx @@ -18,7 +18,7 @@ import { trim, get, } from 'lodash'; -import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { EuiComboBoxProps, EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiFormLabel, @@ -625,25 +625,6 @@ export const ECSMappingEditorForm: React.FC<ECSMappingEditorFormProps> = ({ defaultValue: '', }); - const MultiFields = useMemo( - () => ( - <div> - <OsqueryColumnField - item={item} - index={index} - isLastItem={isLastItem} - // eslint-disable-next-line react-perf/jsx-no-new-object-as-prop - euiFieldProps={{ - // @ts-expect-error update types - options: osquerySchemaOptions, - isDisabled, - }} - /> - </div> - ), - [item, index, isLastItem, osquerySchemaOptions, isDisabled] - ); - const ecsComboBoxEuiFieldProps = useMemo(() => ({ isDisabled }), [isDisabled]); const handleDeleteClick = useCallback(() => { @@ -676,7 +657,19 @@ export const ECSMappingEditorForm: React.FC<ECSMappingEditorFormProps> = ({ </EuiFlexItem> <EuiFlexItem> <EuiFlexGroup alignItems="flexStart" gutterSize="s" wrap> - <ECSFieldWrapper>{MultiFields}</ECSFieldWrapper> + <ECSFieldWrapper> + <OsqueryColumnField + item={item} + index={index} + isLastItem={isLastItem} + // eslint-disable-next-line react-perf/jsx-no-new-object-as-prop + euiFieldProps={{ + // @ts-expect-error update types + options: osquerySchemaOptions, + isDisabled, + }} + /> + </ECSFieldWrapper> {!isDisabled && ( <EuiFlexItem grow={false}> <StyledButtonWrapper> @@ -742,7 +735,7 @@ export const ECSMappingEditorField = React.memo( const fieldsToValidate = prepareEcsFieldsToValidate(fields); // it is always at least 2 - empty fields if (fieldsToValidate.length > 2) { - setTimeout(async () => await trigger('ecs_mapping'), 0); + setTimeout(() => trigger('ecs_mapping'), 0); } }, [fields, query, trigger]); @@ -977,7 +970,7 @@ export const ECSMappingEditorField = React.memo( ); }, [query]); - useLayoutEffect(() => { + useEffect(() => { const ecsList = formData?.ecs_mapping; const lastEcs = formData?.ecs_mapping?.[itemsList?.current.length - 1]; @@ -986,15 +979,16 @@ export const ECSMappingEditorField = React.memo( return; } - // // list contains ecs already, and the last item has values provided + // list contains ecs already, and the last item has values provided if ( - ecsList?.length === itemsList.current.length && - lastEcs?.key?.length && - lastEcs?.result?.value?.length + (ecsList?.length === itemsList.current.length && + lastEcs?.key?.length && + lastEcs?.result?.value?.length) || + !fields?.length ) { return append(defaultEcsFormData); } - }, [append, euiFieldProps?.isDisabled, formData]); + }, [append, fields, formData]); return ( <> diff --git a/x-pack/plugins/osquery/public/plugin.ts b/x-pack/plugins/osquery/public/plugin.ts index 9b8d012e7b084c..ddea34a9361784 100644 --- a/x-pack/plugins/osquery/public/plugin.ts +++ b/x-pack/plugins/osquery/public/plugin.ts @@ -26,7 +26,11 @@ import { LazyOsqueryManagedPolicyEditExtension, LazyOsqueryManagedCustomButtonExtension, } from './fleet_integration'; -import { getLazyOsqueryAction, useIsOsqueryAvailableSimple } from './shared_components'; +import { + getLazyOsqueryAction, + getLazyLiveQueryField, + useIsOsqueryAvailableSimple, +} from './shared_components'; export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginStart> { private kibanaVersion: string; @@ -94,8 +98,10 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt OsqueryAction: getLazyOsqueryAction({ ...core, ...plugins, - storage: this.storage, - kibanaVersion: this.kibanaVersion, + }), + LiveQueryField: getLazyLiveQueryField({ + ...core, + ...plugins, }), isOsqueryAvailable: useIsOsqueryAvailableSimple, }; diff --git a/x-pack/plugins/osquery/public/saved_queries/saved_queries_dropdown.tsx b/x-pack/plugins/osquery/public/saved_queries/saved_queries_dropdown.tsx index eedaf8ac5ce0fe..a00f1b7a80d37f 100644 --- a/x-pack/plugins/osquery/public/saved_queries/saved_queries_dropdown.tsx +++ b/x-pack/plugins/osquery/public/saved_queries/saved_queries_dropdown.tsx @@ -27,7 +27,7 @@ const StyledEuiCodeBlock = styled(EuiCodeBlock)` } `; -interface SavedQueriesDropdownProps { +export interface SavedQueriesDropdownProps { disabled?: boolean; onChange: ( value: diff --git a/x-pack/plugins/osquery/public/shared_components/index.tsx b/x-pack/plugins/osquery/public/shared_components/index.tsx index 8f3d936ab362c1..fee2466a494303 100644 --- a/x-pack/plugins/osquery/public/shared_components/index.tsx +++ b/x-pack/plugins/osquery/public/shared_components/index.tsx @@ -6,4 +6,5 @@ */ export { getLazyOsqueryAction } from './lazy_osquery_action'; +export { getLazyLiveQueryField } from './lazy_live_query_field'; export { useIsOsqueryAvailableSimple } from './osquery_action/use_is_osquery_available_simple'; diff --git a/x-pack/plugins/osquery/public/shared_components/lazy_live_query_field.tsx b/x-pack/plugins/osquery/public/shared_components/lazy_live_query_field.tsx new file mode 100644 index 00000000000000..a8b369b86342fe --- /dev/null +++ b/x-pack/plugins/osquery/public/shared_components/lazy_live_query_field.tsx @@ -0,0 +1,39 @@ +/* + * 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, { lazy, Suspense } from 'react'; +import type { UseFormReturn } from 'react-hook-form'; +import { FormProvider } from 'react-hook-form'; +import type { LiveQueryQueryFieldProps } from '../live_queries/form/live_query_query_field'; +import type { ServicesWrapperProps } from './services_wrapper'; +import ServicesWrapper from './services_wrapper'; + +export const getLazyLiveQueryField = + (services: ServicesWrapperProps['services']) => + // eslint-disable-next-line react/display-name + ({ + formMethods, + ...props + }: LiveQueryQueryFieldProps & { + formMethods: UseFormReturn<{ + label: string; + query: string; + ecs_mapping: Record<string, unknown>; + }>; + }) => { + const LiveQueryField = lazy(() => import('../live_queries/form/live_query_query_field')); + + return ( + <Suspense fallback={null}> + <ServicesWrapper services={services}> + <FormProvider {...formMethods}> + <LiveQueryField {...props} /> + </FormProvider> + </ServicesWrapper> + </Suspense> + ); + }; diff --git a/x-pack/plugins/osquery/public/shared_components/lazy_osquery_action.tsx b/x-pack/plugins/osquery/public/shared_components/lazy_osquery_action.tsx index 5e158c51c02d10..ff464e7782bb78 100644 --- a/x-pack/plugins/osquery/public/shared_components/lazy_osquery_action.tsx +++ b/x-pack/plugins/osquery/public/shared_components/lazy_osquery_action.tsx @@ -6,15 +6,20 @@ */ import React, { lazy, Suspense } from 'react'; +import ServicesWrapper from './services_wrapper'; +import type { ServicesWrapperProps } from './services_wrapper'; +import type { OsqueryActionProps } from './osquery_action'; -// @ts-expect-error update types -// eslint-disable-next-line react/display-name -export const getLazyOsqueryAction = (services) => (props) => { - const OsqueryAction = lazy(() => import('./osquery_action')); +export const getLazyOsqueryAction = + // eslint-disable-next-line react/display-name + (services: ServicesWrapperProps['services']) => (props: OsqueryActionProps) => { + const OsqueryAction = lazy(() => import('./osquery_action')); - return ( - <Suspense fallback={null}> - <OsqueryAction services={services} {...props} /> - </Suspense> - ); -}; + return ( + <Suspense fallback={null}> + <ServicesWrapper services={services}> + <OsqueryAction {...props} /> + </ServicesWrapper> + </Suspense> + ); + }; diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx index 15c6fa645de115..bc039b334a9104 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx +++ b/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import { EuiErrorBoundary, EuiLoadingContent, EuiEmptyPrompt, EuiCode } from '@elastic/eui'; +import { EuiLoadingContent, EuiEmptyPrompt, EuiCode } from '@elastic/eui'; import React, { useMemo } from 'react'; -import { QueryClientProvider } from '@tanstack/react-query'; -import type { CoreStart } from '@kbn/core/public'; + import { AGENT_STATUS_ERROR, EMPTY_PROMPT, @@ -16,17 +15,14 @@ import { PERMISSION_DENIED, SHORT_EMPTY_TITLE, } from './translations'; -import { KibanaContextProvider, useKibana } from '../../common/lib/kibana'; - +import { useKibana } from '../../common/lib/kibana'; import { LiveQuery } from '../../live_queries'; -import { queryClient } from '../../query_client'; import { OsqueryIcon } from '../../components/osquery_icon'; -import { KibanaThemeProvider } from '../../shared_imports'; import { useIsOsqueryAvailable } from './use_is_osquery_available'; -import type { StartPlugins } from '../../types'; -interface OsqueryActionProps { +export interface OsqueryActionProps { agentId?: string; + defaultValues?: {}; formType: 'steps' | 'simple'; hideAgentsField?: boolean; addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement; @@ -35,6 +31,7 @@ interface OsqueryActionProps { const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({ agentId, formType = 'simple', + defaultValues, hideAgentsField, addToTimeline, }) => { @@ -54,7 +51,7 @@ const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({ const { osqueryAvailable, agentFetched, isLoading, policyFetched, policyLoading, agentData } = useIsOsqueryAvailable(agentId); - if (!agentId || (agentFetched && !agentData)) { + if (agentId && agentFetched && !agentData) { return emptyPrompt; } @@ -77,15 +74,15 @@ const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({ ); } - if (isLoading) { + if (agentId && isLoading) { return <EuiLoadingContent lines={10} />; } - if (!policyFetched && policyLoading) { + if (agentId && !policyFetched && policyLoading) { return <EuiLoadingContent lines={10} />; } - if (!osqueryAvailable) { + if (agentId && !osqueryAvailable) { return ( <EuiEmptyPrompt icon={<OsqueryIcon />} @@ -96,7 +93,7 @@ const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({ ); } - if (agentData?.status !== 'online') { + if (agentId && agentData?.status !== 'online') { return ( <EuiEmptyPrompt icon={<OsqueryIcon />} @@ -113,38 +110,14 @@ const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({ agentId={agentId} hideAgentsField={hideAgentsField} addToTimeline={addToTimeline} + {...defaultValues} /> ); }; -export const OsqueryAction = React.memo(OsqueryActionComponent); - -type OsqueryActionWrapperProps = { services: CoreStart & StartPlugins } & OsqueryActionProps; +OsqueryActionComponent.displayName = 'OsqueryAction'; -const OsqueryActionWrapperComponent: React.FC<OsqueryActionWrapperProps> = ({ - services, - agentId, - formType, - hideAgentsField = false, - addToTimeline, -}) => ( - <KibanaThemeProvider theme$={services.theme.theme$}> - <KibanaContextProvider services={services}> - <EuiErrorBoundary> - <QueryClientProvider client={queryClient}> - <OsqueryAction - agentId={agentId} - formType={formType} - hideAgentsField={hideAgentsField} - addToTimeline={addToTimeline} - /> - </QueryClientProvider> - </EuiErrorBoundary> - </KibanaContextProvider> - </KibanaThemeProvider> -); - -const OsqueryActionWrapper = React.memo(OsqueryActionWrapperComponent); +export const OsqueryAction = React.memo(OsqueryActionComponent); // eslint-disable-next-line import/no-default-export -export { OsqueryActionWrapper as default }; +export { OsqueryAction as default }; diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_action/osquery_action.test.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_action/osquery_action.test.tsx index 927d408884d202..ba56cfa0da62dc 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_action/osquery_action.test.tsx +++ b/x-pack/plugins/osquery/public/shared_components/osquery_action/osquery_action.test.tsx @@ -81,13 +81,6 @@ describe('Osquery Action', () => { const { getByText } = renderWithContext(<OsqueryAction agentId={'test'} formType={'steps'} />); expect(getByText(EMPTY_PROMPT)).toBeInTheDocument(); }); - it('should return empty prompt when no agentId', async () => { - spyOsquery(); - mockKibana(); - - const { getByText } = renderWithContext(<OsqueryAction agentId={''} formType={'steps'} />); - expect(getByText(EMPTY_PROMPT)).toBeInTheDocument(); - }); it('should return permission denied when agentFetched and agentData available', async () => { spyOsquery({ agentData: {} }); mockKibana(); diff --git a/x-pack/plugins/osquery/public/shared_components/services_wrapper.tsx b/x-pack/plugins/osquery/public/shared_components/services_wrapper.tsx new file mode 100644 index 00000000000000..7b6949696bbeef --- /dev/null +++ b/x-pack/plugins/osquery/public/shared_components/services_wrapper.tsx @@ -0,0 +1,36 @@ +/* + * 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 { EuiErrorBoundary } from '@elastic/eui'; +import React from 'react'; +import { QueryClientProvider } from '@tanstack/react-query'; +import type { CoreStart } from '@kbn/core/public'; +import { KibanaContextProvider } from '../common/lib/kibana'; + +import { queryClient } from '../query_client'; +import { KibanaThemeProvider } from '../shared_imports'; +import type { StartPlugins } from '../types'; + +export interface ServicesWrapperProps { + services: CoreStart & StartPlugins; + children: React.ReactNode; +} + +const ServicesWrapperComponent: React.FC<ServicesWrapperProps> = ({ services, children }) => ( + <KibanaThemeProvider theme$={services.theme.theme$}> + <KibanaContextProvider services={services}> + <EuiErrorBoundary> + <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> + </EuiErrorBoundary> + </KibanaContextProvider> + </KibanaThemeProvider> +); + +const ServicesWrapper = React.memo(ServicesWrapperComponent); + +// eslint-disable-next-line import/no-default-export +export { ServicesWrapper as default }; diff --git a/x-pack/plugins/osquery/public/types.ts b/x-pack/plugins/osquery/public/types.ts index 69c4befec1b6cc..c19dd10802f320 100644 --- a/x-pack/plugins/osquery/public/types.ts +++ b/x-pack/plugins/osquery/public/types.ts @@ -16,12 +16,13 @@ import type { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, } from '@kbn/triggers-actions-ui-plugin/public'; -import type { getLazyOsqueryAction } from './shared_components'; +import type { getLazyLiveQueryField, getLazyOsqueryAction } from './shared_components'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface OsqueryPluginSetup {} export interface OsqueryPluginStart { OsqueryAction?: ReturnType<typeof getLazyOsqueryAction>; + LiveQueryField?: ReturnType<typeof getLazyLiveQueryField>; isOsqueryAvailable: (props: { agentId: string }) => boolean; } diff --git a/x-pack/plugins/osquery/tsconfig.json b/x-pack/plugins/osquery/tsconfig.json index 4eac1baa43d791..f9cacf100b6918 100644 --- a/x-pack/plugins/osquery/tsconfig.json +++ b/x-pack/plugins/osquery/tsconfig.json @@ -6,6 +6,7 @@ "declaration": true, "declarationMap": true }, + "exclude": ["cypress.config.ts"], "include": [ // add all the folders contains files to be compiled "common/**/*", @@ -13,6 +14,7 @@ "scripts/**/*", "scripts/**/**.json", "server/**/*", + "cypress.config.ts", "../../../typings/**/*", // ECS and Osquery schema files "public/common/schemas/*/**.json", diff --git a/x-pack/plugins/reporting/server/config/create_config.test.ts b/x-pack/plugins/reporting/server/config/create_config.test.ts index d2fd645fcc26d6..b884151a500f46 100644 --- a/x-pack/plugins/reporting/server/config/create_config.test.ts +++ b/x-pack/plugins/reporting/server/config/create_config.test.ts @@ -79,7 +79,12 @@ describe('Reporting server createConfig$', () => { "capture": Object { "maxAttempts": 1, }, - "csv": Object {}, + "csv": Object { + "scroll": Object { + "duration": "30s", + "size": 500, + }, + }, "encryptionKey": "iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii", "index": ".reporting", "kibanaServer": Object { diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.ts index 8b5f0e5395827b..d7e5f4be33f8e1 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.ts @@ -16,11 +16,11 @@ export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadCSV>> = ( parentLogger ) => { const config = reporting.getConfig(); + const encryptionKey = config.get('encryptionKey'); + const csvConfig = config.get('csv'); return async function runTask(jobId, job, cancellationToken, stream) { const logger = parentLogger.get(`execute-job:${jobId}`); - - const encryptionKey = config.get('encryptionKey'); const headers = await decryptJobHeaders(encryptionKey, job.headers, logger); const fakeRequest = reporting.getFakeRequest({ headers }, job.spaceId, logger); const uiSettings = await reporting.getUiSettingsClient(fakeRequest, logger); @@ -44,7 +44,7 @@ export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadCSV>> = ( const csv = new CsvGenerator( job, - config, + csvConfig, clients, dependencies, cancellationToken, diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts index 0ce8af4c1aa5a5..d339d2b12bad0f 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts @@ -7,9 +7,7 @@ import { errors as esErrors } from '@elastic/elasticsearch'; import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; -import type { Logger, IScopedClusterClient, IUiSettingsClient } from '@kbn/core/server'; -import { identity, range } from 'lodash'; -import * as Rx from 'rxjs'; +import type { IScopedClusterClient, IUiSettingsClient, Logger } from '@kbn/core/server'; import { elasticsearchServiceMock, loggingSystemMock, @@ -21,14 +19,17 @@ import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_ import { IScopedSearchClient } from '@kbn/data-plugin/server'; import { dataPluginMock } from '@kbn/data-plugin/server/mocks'; import { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common'; -import { Writable } from 'stream'; -import { ReportingConfig } from '../../..'; +import { identity, range } from 'lodash'; +import * as Rx from 'rxjs'; +import type { Writable } from 'stream'; +import type { DeepPartial } from 'utility-types'; import { CancellationToken } from '../../../../common/cancellation_token'; import { UI_SETTINGS_CSV_QUOTE_VALUES, UI_SETTINGS_CSV_SEPARATOR, UI_SETTINGS_DATEFORMAT_TZ, } from '../../../../common/constants'; +import { ReportingConfigType } from '../../../config'; import { createMockConfig, createMockConfigSchema } from '../../../test_helpers'; import { JobParamsCSV } from '../types'; import { CsvGenerator } from './generate_csv'; @@ -39,7 +40,7 @@ const createMockJob = (baseObj: any = {}): JobParamsCSV => ({ let mockEsClient: IScopedClusterClient; let mockDataClient: IScopedSearchClient; -let mockConfig: ReportingConfig; +let mockConfig: ReportingConfigType['csv']; let mockLogger: jest.Mocked<Logger>; let uiSettingsClient: IUiSettingsClient; let stream: jest.Mocked<Writable>; @@ -79,6 +80,11 @@ const mockFieldFormatsRegistry = { .mockImplementation(() => ({ id: 'string', convert: jest.fn().mockImplementation(identity) })), } as unknown as FieldFormatsRegistry; +const getMockConfig = (properties: DeepPartial<ReportingConfigType> = {}) => { + const config = createMockConfig(createMockConfigSchema(properties)); + return config.get('csv'); +}; + beforeEach(async () => { content = ''; stream = { write: jest.fn((chunk) => (content += chunk)) } as unknown as typeof stream; @@ -100,16 +106,14 @@ beforeEach(async () => { } }); - mockConfig = createMockConfig( - createMockConfigSchema({ - csv: { - checkForFormulas: true, - escapeFormulaValues: true, - maxSizeBytes: 180000, - scroll: { size: 500, duration: '30s' }, - }, - }) - ); + mockConfig = getMockConfig({ + csv: { + checkForFormulas: true, + escapeFormulaValues: true, + maxSizeBytes: 180000, + scroll: { size: 500, duration: '30s' }, + }, + }); searchSourceMock.getField = jest.fn((key: string) => { switch (key) { @@ -231,17 +235,14 @@ it('calculates the bytes of the content', async () => { it('warns if max size was reached', async () => { const TEST_MAX_SIZE = 500; - - mockConfig = createMockConfig( - createMockConfigSchema({ - csv: { - checkForFormulas: true, - escapeFormulaValues: true, - maxSizeBytes: TEST_MAX_SIZE, - scroll: { size: 500, duration: '30s' }, - }, - }) - ); + mockConfig = getMockConfig({ + csv: { + checkForFormulas: true, + escapeFormulaValues: true, + maxSizeBytes: TEST_MAX_SIZE, + scroll: { size: 500, duration: '30s' }, + }, + }); mockDataClient.search = jest.fn().mockImplementation(() => Rx.of({ @@ -300,6 +301,7 @@ it('uses the scrollId to page all the data', async () => { }, }) ); + mockEsClient.asCurrentUser.scroll = jest.fn().mockResolvedValue({ hits: { hits: range(0, HITS_TOTAL / 10).map(() => ({ @@ -335,7 +337,7 @@ it('uses the scrollId to page all the data', async () => { expect(mockDataClient.search).toHaveBeenCalledTimes(1); expect(mockDataClient.search).toBeCalledWith( { params: { body: {}, ignore_throttled: undefined, scroll: '30s', size: 500 } }, - { strategy: 'es' } + { strategy: 'es', transport: { maxRetries: 0, requestTimeout: '30s' } } ); // `scroll` and `clearScroll` must be called with scroll ID in the post body! @@ -729,16 +731,14 @@ describe('formulas', () => { }); it('can check for formulas, without escaping them', async () => { - mockConfig = createMockConfig( - createMockConfigSchema({ - csv: { - checkForFormulas: true, - escapeFormulaValues: false, - maxSizeBytes: 180000, - scroll: { size: 500, duration: '30s' }, - }, - }) - ); + mockConfig = getMockConfig({ + csv: { + checkForFormulas: true, + escapeFormulaValues: false, + maxSizeBytes: 180000, + scroll: { size: 500, duration: '30s' }, + }, + }); mockDataClient.search = jest.fn().mockImplementation(() => Rx.of({ rawResponse: { @@ -804,8 +804,15 @@ it('can override ignoring frozen indices', async () => { await generateCsv.generateData(); expect(mockDataClient.search).toBeCalledWith( - { params: { body: {}, ignore_throttled: false, scroll: '30s', size: 500 } }, - { strategy: 'es' } + { + params: { + body: {}, + ignore_throttled: false, + scroll: '30s', + size: 500, + }, + }, + { strategy: 'es', transport: { maxRetries: 0, requestTimeout: '30s' } } ); }); diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts index bbb8fee7cd01db..c72fce0f12bca6 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts @@ -24,11 +24,11 @@ import type { } from '@kbn/field-formats-plugin/common'; import { lastValueFrom } from 'rxjs'; import type { Writable } from 'stream'; -import type { ReportingConfig } from '../../..'; import type { CancellationToken } from '../../../../common/cancellation_token'; import { CONTENT_TYPE_CSV } from '../../../../common/constants'; import { AuthenticationExpiredError, ReportingError } from '../../../../common/errors'; import { byteSizeValueToNumber } from '../../../../common/schema_utils'; +import { ReportingConfigType } from '../../../config'; import type { TaskRunResult } from '../../../lib/tasks'; import type { JobParamsCSV } from '../types'; import { CsvExportSettings, getExportSettings } from './get_export_settings'; @@ -53,7 +53,7 @@ export class CsvGenerator { constructor( private job: Omit<JobParamsCSV, 'version'>, - private config: ReportingConfig, + private config: ReportingConfigType['csv'], private clients: Clients, private dependencies: Dependencies, private cancellationToken: CancellationToken, @@ -84,7 +84,13 @@ export class CsvGenerator { try { results = ( await lastValueFrom( - this.clients.data.search(searchParams, { strategy: ES_SEARCH_STRATEGY }) + this.clients.data.search(searchParams, { + strategy: ES_SEARCH_STRATEGY, + transport: { + maxRetries: 0, // retrying reporting jobs is handled in the task manager scheduling logic + requestTimeout: this.config.scroll.duration, + }, + }) ) ).rawResponse as estypes.SearchResponse<unknown>; } catch (err) { diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.test.ts index 863425822d7dd5..96bde310b2c056 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.test.ts @@ -22,7 +22,7 @@ import { getExportSettings } from './get_export_settings'; describe('getExportSettings', () => { let uiSettingsClient: IUiSettingsClient; - const config = createMockConfig(createMockConfigSchema({})); + const config = createMockConfig(createMockConfigSchema({})).get('csv'); const logger = loggingSystemMock.createLogger(); beforeEach(() => { @@ -55,8 +55,8 @@ describe('getExportSettings', () => { "includeFrozen": false, "maxSizeBytes": undefined, "scroll": Object { - "duration": undefined, - "size": undefined, + "duration": "30s", + "size": 500, }, "separator": ",", "timezone": "UTC", diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.ts index dff80970dff74e..1c5dc1983ab74b 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.ts @@ -8,7 +8,6 @@ import { ByteSizeValue } from '@kbn/config-schema'; import type { IUiSettingsClient, Logger } from '@kbn/core/server'; import { createEscapeValue } from '@kbn/data-plugin/common'; -import { ReportingConfig } from '../../..'; import { CSV_BOM_CHARS, UI_SETTINGS_CSV_QUOTE_VALUES, @@ -16,6 +15,7 @@ import { UI_SETTINGS_DATEFORMAT_TZ, UI_SETTINGS_SEARCH_INCLUDE_FROZEN, } from '../../../../common/constants'; +import { ReportingConfigType } from '../../../config'; export interface CsvExportSettings { timezone: string; @@ -34,7 +34,7 @@ export interface CsvExportSettings { export const getExportSettings = async ( client: IUiSettingsClient, - config: ReportingConfig, + config: ReportingConfigType['csv'], timezone: string | undefined, logger: Logger ): Promise<CsvExportSettings> => { @@ -60,21 +60,21 @@ export const getExportSettings = async ( client.get(UI_SETTINGS_CSV_QUOTE_VALUES), ]); - const escapeFormulaValues = config.get('csv', 'escapeFormulaValues'); + const escapeFormulaValues = config.escapeFormulaValues; const escapeValue = createEscapeValue(quoteValues, escapeFormulaValues); - const bom = config.get('csv', 'useByteOrderMarkEncoding') ? CSV_BOM_CHARS : ''; + const bom = config.useByteOrderMarkEncoding ? CSV_BOM_CHARS : ''; return { timezone: setTimezone, scroll: { - size: config.get('csv', 'scroll', 'size'), - duration: config.get('csv', 'scroll', 'duration'), + size: config.scroll.size, + duration: config.scroll.duration, }, bom, includeFrozen, separator, - maxSizeBytes: config.get('csv', 'maxSizeBytes'), - checkForFormulas: config.get('csv', 'checkForFormulas'), + maxSizeBytes: config.maxSizeBytes, + checkForFormulas: config.checkForFormulas, escapeFormulaValues, escapeValue, }; diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts index 31924d0d89cf58..7887c55b14b63e 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts @@ -30,7 +30,7 @@ export const runTaskFnFactory: RunTaskFnFactory<ImmediateExecuteFn> = function e reporting, parentLogger ) { - const config = reporting.getConfig(); + const config = reporting.getConfig().get('csv'); const logger = parentLogger.get('execute-job'); return async function runTask(_jobId, immediateJobParams, context, stream, req) { @@ -39,9 +39,9 @@ export const runTaskFnFactory: RunTaskFnFactory<ImmediateExecuteFn> = function e ...immediateJobParams, }; + const dataPluginStart = await reporting.getDataService(); const savedObjectsClient = (await context.core).savedObjects.client; const uiSettings = await reporting.getUiSettingsServiceFactory(savedObjectsClient); - const dataPluginStart = await reporting.getDataService(); const fieldFormatsRegistry = await getFieldFormats().fieldFormatServiceFactory(uiSettings); const [es, searchSourceStart] = await Promise.all([ diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index cdb0c7ac44d70d..aa1640274e4ce9 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -113,6 +113,7 @@ export const createMockConfigSchema = ( ...overrides.queue, }, csv: { + scroll: { size: 500, duration: '30s' }, ...overrides.csv, }, roles: { diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts index 3c3edf4e988ec7..59db45aff0fa2f 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts @@ -19,6 +19,7 @@ const createAlertsClientMock = () => { bulkUpdate: jest.fn(), find: jest.fn(), getFeatureIdsByRegistrationContexts: jest.fn(), + getBrowserFields: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts index 348dfebed1fcd7..31731cecbeccb2 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts @@ -30,6 +30,7 @@ import { } from '@kbn/alerting-plugin/server'; import { Logger, ElasticsearchClient, EcsEventOutcome } from '@kbn/core/server'; import { AuditLogger } from '@kbn/security-plugin/server'; +import { IndexPatternsFetcher } from '@kbn/data-plugin/server'; import { alertAuditEvent, operationAlertAuditActionMap } from './audit_events'; import { ALERT_WORKFLOW_STATUS, @@ -40,6 +41,8 @@ import { import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; import { Dataset, IRuleDataService } from '../rule_data_plugin_service'; import { getAuthzFilter, getSpacesFilter } from '../lib'; +import { fieldDescriptorToBrowserFieldMapper } from './browser_fields'; +import { BrowserFields } from '../types'; // TODO: Fix typings https://github.com/elastic/kibana/issues/101776 type NonNullableProps<Obj extends {}, Props extends keyof Obj> = Omit<Obj, Props> & { @@ -716,4 +719,23 @@ export class AlertsClient { throw Boom.failedDependency(errMessage); } } + + async getBrowserFields({ + indices, + metaFields, + allowNoIndex, + }: { + indices: string[]; + metaFields: string[]; + allowNoIndex: boolean; + }): Promise<BrowserFields> { + const indexPatternsFetcherAsInternalUser = new IndexPatternsFetcher(this.esClient); + const { fields } = await indexPatternsFetcherAsInternalUser.getFieldsForWildcard({ + pattern: indices, + metaFields, + fieldCapsOptions: { allow_no_indices: allowNoIndex }, + }); + + return fieldDescriptorToBrowserFieldMapper(fields); + } } diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/browser_fields/index.ts b/x-pack/plugins/rule_registry/server/alert_data_client/browser_fields/index.ts new file mode 100644 index 00000000000000..074c3f60006c87 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/browser_fields/index.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FieldDescriptor } from '@kbn/data-views-plugin/server'; +import { BrowserField, BrowserFields } from '../../types'; + +const getFieldCategory = (fieldCapability: FieldDescriptor) => { + const name = fieldCapability.name.split('.'); + + if (name.length === 1) { + return 'base'; + } + + return name[0]; +}; + +const browserFieldFactory = ( + fieldCapability: FieldDescriptor, + category: string +): { [fieldName in string]: BrowserField } => { + return { + [fieldCapability.name]: { + ...fieldCapability, + category, + }, + }; +}; + +export const fieldDescriptorToBrowserFieldMapper = (fields: FieldDescriptor[]): BrowserFields => { + return fields.reduce((browserFields: BrowserFields, field: FieldDescriptor) => { + const category = getFieldCategory(field); + const browserField = browserFieldFactory(field, category); + + if (browserFields[category]) { + browserFields[category] = { fields: { ...browserFields[category].fields, ...browserField } }; + } else { + browserFields[category] = { fields: browserField }; + } + + return browserFields; + }, {}); +}; diff --git a/x-pack/plugins/rule_registry/server/routes/__mocks__/request_responses.ts b/x-pack/plugins/rule_registry/server/routes/__mocks__/request_responses.ts index b750b37aa51b5b..3fad5f9309532a 100644 --- a/x-pack/plugins/rule_registry/server/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/rule_registry/server/routes/__mocks__/request_responses.ts @@ -39,3 +39,10 @@ export const getReadFeatureIdsRequest = () => path: `${BASE_RAC_ALERTS_API_PATH}/_feature_ids`, query: { registrationContext: ['security'] }, }); + +export const getO11yBrowserFields = () => + requestMock.create({ + method: 'get', + path: `${BASE_RAC_ALERTS_API_PATH}/browser_fields`, + query: { featureIds: ['apm', 'logs'] }, + }); diff --git a/x-pack/plugins/rule_registry/server/routes/get_browser_fields_by_feature_id.test.ts b/x-pack/plugins/rule_registry/server/routes/get_browser_fields_by_feature_id.test.ts new file mode 100644 index 00000000000000..49e559c9f4c24f --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/get_browser_fields_by_feature_id.test.ts @@ -0,0 +1,65 @@ +/* + * 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 { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; +import { getBrowserFieldsByFeatureId } from './get_browser_fields_by_feature_id'; +import { requestContextMock } from './__mocks__/request_context'; +import { getO11yBrowserFields } from './__mocks__/request_responses'; +import { requestMock, serverMock } from './__mocks__/server'; + +describe('getBrowserFieldsByFeatureId', () => { + let server: ReturnType<typeof serverMock.create>; + let { clients, context } = requestContextMock.createTools(); + const path = `${BASE_RAC_ALERTS_API_PATH}/browser_fields`; + + beforeEach(async () => { + server = serverMock.create(); + ({ clients, context } = requestContextMock.createTools()); + }); + + describe('when racClient returns o11y indices', () => { + beforeEach(() => { + clients.rac.getAuthorizedAlertsIndices.mockResolvedValue([ + '.alerts-observability.logs.alerts-default', + ]); + + getBrowserFieldsByFeatureId(server.router); + }); + + test('route registered', async () => { + const response = await server.inject(getO11yBrowserFields(), context); + + expect(response.status).toEqual(200); + }); + + test('rejects invalid featureId type', async () => { + await expect( + server.inject( + requestMock.create({ + method: 'get', + path, + query: { featureIds: undefined }, + }), + context + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Request was rejected with message: 'Invalid value \\"undefined\\" supplied to \\"featureIds\\"'"` + ); + }); + + test('returns error status if rac client "getAuthorizedAlertsIndices" fails', async () => { + clients.rac.getAuthorizedAlertsIndices.mockRejectedValue(new Error('Unable to get index')); + const response = await server.inject(getO11yBrowserFields(), context); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + attributes: { success: false }, + message: 'Unable to get index', + }); + }); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/routes/get_browser_fields_by_feature_id.ts b/x-pack/plugins/rule_registry/server/routes/get_browser_fields_by_feature_id.ts new file mode 100644 index 00000000000000..6b2d59c824ab3d --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/get_browser_fields_by_feature_id.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from '@kbn/core/server'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import * as t from 'io-ts'; + +import { RacRequestHandlerContext } from '../types'; +import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; +import { buildRouteValidation } from './utils/route_validation'; + +export const getBrowserFieldsByFeatureId = (router: IRouter<RacRequestHandlerContext>) => { + router.get( + { + path: `${BASE_RAC_ALERTS_API_PATH}/browser_fields`, + validate: { + query: buildRouteValidation( + t.exact( + t.type({ + featureIds: t.union([t.string, t.array(t.string)]), + }) + ) + ), + }, + options: { + tags: ['access:rac'], + }, + }, + async (context, request, response) => { + try { + const racContext = await context.rac; + const alertsClient = await racContext.getAlertsClient(); + const { featureIds = [] } = request.query; + + const indices = await alertsClient.getAuthorizedAlertsIndices( + Array.isArray(featureIds) ? featureIds : [featureIds] + ); + const o11yIndices = + indices?.filter((index) => index.startsWith('.alerts-observability')) ?? []; + if (o11yIndices.length === 0) { + return response.notFound({ + body: { + message: `No alerts-observability indices found for featureIds [${featureIds}]`, + attributes: { success: false }, + }, + }); + } + + const browserFields = await alertsClient.getBrowserFields({ + indices: o11yIndices, + metaFields: ['_id', '_index'], + allowNoIndex: true, + }); + + return response.ok({ + body: browserFields, + }); + } catch (error) { + const formatedError = transformError(error); + const contentType = { + 'content-type': 'application/json', + }; + const defaultedHeaders = { + ...contentType, + }; + + return response.customError({ + headers: defaultedHeaders, + statusCode: formatedError.statusCode, + body: { + message: formatedError.message, + attributes: { + success: false, + }, + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/rule_registry/server/routes/index.ts b/x-pack/plugins/rule_registry/server/routes/index.ts index 638fb4e432412e..a693de9b2fa4c4 100644 --- a/x-pack/plugins/rule_registry/server/routes/index.ts +++ b/x-pack/plugins/rule_registry/server/routes/index.ts @@ -13,6 +13,7 @@ import { getAlertsIndexRoute } from './get_alert_index'; import { bulkUpdateAlertsRoute } from './bulk_update_alerts'; import { findAlertsByQueryRoute } from './find'; import { getFeatureIdsByRegistrationContexts } from './get_feature_ids_by_registration_contexts'; +import { getBrowserFieldsByFeatureId } from './get_browser_fields_by_feature_id'; export function defineRoutes(router: IRouter<RacRequestHandlerContext>) { getAlertByIdRoute(router); @@ -21,4 +22,5 @@ export function defineRoutes(router: IRouter<RacRequestHandlerContext>) { bulkUpdateAlertsRoute(router); findAlertsByQueryRoute(router); getFeatureIdsByRegistrationContexts(router); + getBrowserFieldsByFeatureId(router); } diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts index 2ed80cb02c0d3e..fdd0b1c931bd1e 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts @@ -337,6 +337,7 @@ export class ResourceInstaller { rollover_alias: primaryNamespacedAlias, }, 'index.mapping.total_fields.limit': 1700, + auto_expand_replicas: '0-1', }, mappings: { dynamic: false, diff --git a/x-pack/plugins/rule_registry/server/types.ts b/x-pack/plugins/rule_registry/server/types.ts index 6a7d5b849c771e..f466a7f8cf495a 100644 --- a/x-pack/plugins/rule_registry/server/types.ts +++ b/x-pack/plugins/rule_registry/server/types.ts @@ -13,6 +13,7 @@ import { RuleTypeState, } from '@kbn/alerting-plugin/common'; import { RuleExecutorOptions, RuleExecutorServices, RuleType } from '@kbn/alerting-plugin/server'; +import { FieldSpec } from '@kbn/data-plugin/common'; import { AlertsClient } from './alert_data_client/alerts_client'; type SimpleAlertType< @@ -71,3 +72,11 @@ export interface RacApiRequestHandlerContext { export type RacRequestHandlerContext = CustomRequestHandlerContext<{ rac: RacApiRequestHandlerContext; }>; + +export type BrowserField = FieldSpec & { + category: string; +}; + +export type BrowserFields = { + [category in string]: { fields: { [fieldName in string]: BrowserField } }; +}; diff --git a/x-pack/plugins/screenshotting/README.md b/x-pack/plugins/screenshotting/README.md index aefa4cc90762b5..3a3ea87448e647 100644 --- a/x-pack/plugins/screenshotting/README.md +++ b/x-pack/plugins/screenshotting/README.md @@ -89,7 +89,6 @@ Option | Required | Default | Description `layout` | no | `{}` | Page layout parameters describing characteristics of the capturing screenshot (e.g., dimensions, zoom, etc.). `request` | no | _none_ | Kibana Request reference to extract headers from. `timeouts` | no | _none_ | Timeouts for each phase of the screenshot. -`timeouts.loadDelay` | no | `3000` | The amount of time in milliseconds before taking a screenshot when visualizations are not evented. All visualizations that ship with Kibana are evented, so this setting should not have much effect. If you are seeing empty images instead of visualizations, try increasing this value. `timeouts.openUrl` | no | `60000` | The timeout in milliseconds to allow the Chromium browser to wait for the "Loading…" screen to dismiss and find the initial data for the page. If the time is exceeded, a screenshot is captured showing the current page, and the result structure contains an error message. `timeouts.renderComplete` | no | `30000` | The timeout in milliseconds to allow the Chromium browser to wait for all visualizations to fetch and render the data. If the time is exceeded, a screenshot is captured showing the current page, and the result structure contains an error message. `timeouts.waitForElements` | no | `30000` | The timeout in milliseconds to allow the Chromium browser to wait for all visualization panels to load on the page. If the time is exceeded, a screenshot is captured showing the current page, and the result structure contains an error message. diff --git a/x-pack/plugins/screenshotting/server/config/schema.test.ts b/x-pack/plugins/screenshotting/server/config/schema.test.ts index 58fb4b5ab559e8..c2febf59062490 100644 --- a/x-pack/plugins/screenshotting/server/config/schema.test.ts +++ b/x-pack/plugins/screenshotting/server/config/schema.test.ts @@ -20,7 +20,6 @@ describe('ConfigSchema', () => { }, }, "capture": Object { - "loadDelay": "PT3S", "timeouts": Object { "openUrl": "PT1M", "renderComplete": "PT30S", @@ -81,7 +80,6 @@ describe('ConfigSchema', () => { }, }, "capture": Object { - "loadDelay": "PT3S", "timeouts": Object { "openUrl": "PT1M", "renderComplete": "PT30S", diff --git a/x-pack/plugins/screenshotting/server/config/schema.ts b/x-pack/plugins/screenshotting/server/config/schema.ts index 1e103a6b6e4d08..4900a5c9d775e5 100644 --- a/x-pack/plugins/screenshotting/server/config/schema.ts +++ b/x-pack/plugins/screenshotting/server/config/schema.ts @@ -81,9 +81,7 @@ export const ConfigSchema = schema.object({ }), }), zoom: schema.number({ defaultValue: 2 }), - loadDelay: schema.oneOf([schema.number(), schema.duration()], { - defaultValue: moment.duration({ seconds: 3 }), - }), + loadDelay: schema.maybe(schema.oneOf([schema.number(), schema.duration()])), // deprecated, unused }), poolSize: schema.number({ defaultValue: 1, min: 1 }), }); diff --git a/x-pack/plugins/screenshotting/server/screenshots/index.test.ts b/x-pack/plugins/screenshotting/server/screenshots/index.test.ts index 11ce25e0f86f13..70aca733a03d1f 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/index.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/index.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { Logger, PackageInfo } from '@kbn/core/server'; import { httpServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { lastValueFrom, of, throwError } from 'rxjs'; @@ -14,12 +15,11 @@ import { SCREENSHOTTING_EXPRESSION, SCREENSHOTTING_EXPRESSION_INPUT, } from '../../common'; -import type { CloudSetup } from '@kbn/cloud-plugin/server'; +import * as errors from '../../common/errors'; import type { HeadlessChromiumDriverFactory } from '../browsers'; import { createMockBrowserDriver, createMockBrowserDriverFactory } from '../browsers/mock'; import type { ConfigType } from '../config'; import type { PngScreenshotOptions } from '../formats'; -import * as errors from '../../common/errors'; import * as Layouts from '../layouts/create_layout'; import { createMockLayout } from '../layouts/mock'; import { CONTEXT_ELEMENTATTRIBUTES } from './constants'; @@ -72,7 +72,6 @@ describe('Screenshot Observable Pipeline', () => { waitForElements: 30000, renderComplete: 30000, }, - loadDelay: 5000000000, zoom: 2, }, networkPolicy: { enabled: false, rules: [] }, @@ -125,13 +124,13 @@ describe('Screenshot Observable Pipeline', () => { }); it('captures screenshot of an expression', async () => { - await screenshots - .getScreenshots({ + await lastValueFrom( + screenshots.getScreenshots({ ...options, expression: 'kibana', input: 'something', } as PngScreenshotOptions) - .toPromise(); + ); expect(driver.open).toHaveBeenCalledTimes(1); expect(driver.open).toHaveBeenCalledWith( @@ -148,7 +147,7 @@ describe('Screenshot Observable Pipeline', () => { describe('error handling', () => { it('recovers if waitForSelector fails', async () => { - driver.waitForSelector.mockImplementation((selectorArg: string) => { + driver.waitForSelector.mockImplementation(() => { throw new Error('Mock error!'); }); const result = await lastValueFrom( @@ -169,14 +168,14 @@ describe('Screenshot Observable Pipeline', () => { driverFactory.createPage.mockReturnValue( of({ driver, - error$: throwError('Instant timeout has fired!'), + error$: throwError(() => 'Instant timeout has fired!'), close: () => of({}), }) ); - await expect(screenshots.getScreenshots(options).toPromise()).rejects.toMatchInlineSnapshot( - `"Instant timeout has fired!"` - ); + await expect( + lastValueFrom(screenshots.getScreenshots(options)) + ).rejects.toMatchInlineSnapshot(`"Instant timeout has fired!"`); }); it(`uses defaults for element positions and size when Kibana page is not ready`, async () => { diff --git a/x-pack/plugins/screenshotting/server/screenshots/index.ts b/x-pack/plugins/screenshotting/server/screenshots/index.ts index 57f8440c348172..0c6c6f409f848a 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/index.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/index.ts @@ -203,7 +203,6 @@ export class Screenshots { openUrl: 60000, waitForElements: 30000, renderComplete: 30000, - loadDelay: 3000, }, urls: [], } diff --git a/x-pack/plugins/screenshotting/server/screenshots/observable.test.ts b/x-pack/plugins/screenshotting/server/screenshots/observable.test.ts index cb0fa6720ff7d6..363c30ad83f339 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/observable.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/observable.test.ts @@ -6,7 +6,7 @@ */ import { loggingSystemMock } from '@kbn/core/server/mocks'; -import { interval, of, throwError } from 'rxjs'; +import { interval, lastValueFrom, of, throwError } from 'rxjs'; import { map } from 'rxjs/operators'; import { createMockBrowserDriver } from '../browsers/mock'; import type { ConfigType } from '../config'; @@ -26,7 +26,6 @@ describe('ScreenshotObservableHandler', () => { config = { capture: { timeouts: { openUrl: 30000, waitForElements: 30000, renderComplete: 30000 }, - loadDelay: 5000, zoom: 13, }, } as ConfigType; @@ -55,14 +54,14 @@ describe('ScreenshotObservableHandler', () => { }) ); - const testPipeline = () => test$.toPromise(); + const testPipeline = () => lastValueFrom(test$); await expect(testPipeline).rejects.toMatchInlineSnapshot( `[Error: Screenshotting encountered a timeout error: "Test Config" took longer than 0.2 seconds. You may need to increase "xpack.screenshotting.testConfig" in kibana.yml.]` ); }); it('catches other Errors', async () => { - const test$ = throwError(new Error(`Test Error to Throw`)).pipe( + const test$ = throwError(() => new Error(`Test Error to Throw`)).pipe( screenshots.waitUntil({ timeoutValue: 200, label: 'Test Config', @@ -70,7 +69,7 @@ describe('ScreenshotObservableHandler', () => { }) ); - const testPipeline = () => test$.toPromise(); + const testPipeline = () => lastValueFrom(test$); await expect(testPipeline).rejects.toMatchInlineSnapshot( `[Error: The "Test Config" phase encountered an error: Error: Test Error to Throw]` ); @@ -85,7 +84,7 @@ describe('ScreenshotObservableHandler', () => { }) ); - await expect(test$.toPromise()).resolves.toBe(`nice to see you`); + await expect(lastValueFrom(test$)).resolves.toBe(`nice to see you`); }); }); @@ -104,7 +103,7 @@ describe('ScreenshotObservableHandler', () => { }) ); - await expect(test$.toPromise()).rejects.toMatchInlineSnapshot( + await expect(lastValueFrom(test$)).rejects.toMatchInlineSnapshot( `[Error: Browser was closed unexpectedly! Check the server logs for more info.]` ); }); @@ -117,7 +116,7 @@ describe('ScreenshotObservableHandler', () => { }) ); - await expect(test$.toPromise()).resolves.toBe(234455); + await expect(lastValueFrom(test$)).resolves.toBe(234455); }); }); }); diff --git a/x-pack/plugins/screenshotting/server/screenshots/observable.ts b/x-pack/plugins/screenshotting/server/screenshots/observable.ts index efd0974612c59b..f5662ee920bf4b 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/observable.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/observable.ts @@ -124,7 +124,6 @@ const getTimeouts = (captureConfig: ConfigType['capture']) => ({ configValue: `xpack.screenshotting.capture.timeouts.renderComplete`, label: 'render complete', }, - loadDelay: toNumber(captureConfig.loadDelay), }); export class ScreenshotObservableHandler { @@ -132,7 +131,7 @@ export class ScreenshotObservableHandler { constructor( private readonly driver: HeadlessChromiumDriver, - private readonly config: ConfigType, + config: ConfigType, private readonly eventLogger: EventLogger, private readonly layout: Layout, private options: ScreenshotObservableOptions @@ -222,12 +221,7 @@ export class ScreenshotObservableHandler { throw error; } - await waitForRenderComplete( - driver, - eventLogger, - toNumber(this.config.capture.loadDelay), - layout - ); + await waitForRenderComplete(driver, eventLogger, layout); }).pipe( mergeMap(() => forkJoin({ diff --git a/x-pack/plugins/screenshotting/server/screenshots/wait_for_render.ts b/x-pack/plugins/screenshotting/server/screenshots/wait_for_render.ts index 8cf8174be152fc..ed4ad83736d42e 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/wait_for_render.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/wait_for_render.ts @@ -13,7 +13,6 @@ import { Actions, EventLogger } from './event_logger'; export const waitForRenderComplete = async ( browser: HeadlessChromiumDriver, eventLogger: EventLogger, - loadDelay: number, layout: Layout ) => { const spanEnd = eventLogger.logScreenshottingEvent( @@ -22,54 +21,35 @@ export const waitForRenderComplete = async ( 'wait' ); - return await browser - .evaluate( - { - fn: (selector, visLoadDelay) => { - // wait for visualizations to finish loading - const visualizations: NodeListOf<Element> = document.querySelectorAll(selector); - const visCount = visualizations.length; - const renderedTasks = []; - - function waitForRender(visualization: Element) { - return new Promise<void>((resolve) => { - visualization.addEventListener('renderComplete', () => resolve()); - }); - } - - function waitForRenderDelay() { - return new Promise((resolve) => { - setTimeout(resolve, visLoadDelay); - }); + await browser.evaluate( + { + fn: async (selector) => { + const visualizations: NodeListOf<Element> = document.querySelectorAll(selector); + const visCount = visualizations.length; + const renderedTasks = []; + + function waitForRender(visualization: Element) { + return new Promise<void>((resolve) => { + visualization.addEventListener('renderComplete', () => resolve()); + }); + } + + for (let i = 0; i < visCount; i++) { + const visualization = visualizations[i]; + const isRendered = visualization.getAttribute('data-render-complete'); + + if (isRendered === 'false') { + renderedTasks.push(waitForRender(visualization)); } + } - for (let i = 0; i < visCount; i++) { - const visualization = visualizations[i]; - const isRendered = visualization.getAttribute('data-render-complete'); - - if (isRendered === 'disabled') { - renderedTasks.push(waitForRenderDelay()); - } else if (isRendered === 'false') { - renderedTasks.push(waitForRender(visualization)); - } - } - - // The renderComplete fires before the visualizations are in the DOM, so - // we wait for the event loop to flush before telling reporting to continue. This - // seems to correct a timing issue that was causing reporting to occasionally - // capture the first visualization before it was actually in the DOM. - // Note: 100 proved too short, see https://github.com/elastic/kibana/issues/22581, - // bumping to 250. - const hackyWaitForVisualizations = () => new Promise((r) => setTimeout(r, 250)); - - return Promise.all(renderedTasks).then(hackyWaitForVisualizations); - }, - args: [layout.selectors.renderComplete, loadDelay], + return await Promise.all(renderedTasks); }, - { context: CONTEXT_WAITFORRENDER }, - eventLogger.kbnLogger - ) - .then(() => { - spanEnd(); - }); + args: [layout.selectors.renderComplete], + }, + { context: CONTEXT_WAITFORRENDER }, + eventLogger.kbnLogger + ); + + spanEnd(); }; diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 6f3958cbb54e1b..622c74efd82814 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -113,7 +113,7 @@ export enum SecurityPageName { noPage = '', overview = 'overview', policies = 'policy', - responseActions = 'response_actions', + actionHistory = 'action_history', rules = 'rules', rulesCreate = 'rules-create', sessions = 'sessions', @@ -159,7 +159,7 @@ export const EVENT_FILTERS_PATH = `${MANAGEMENT_PATH}/event_filters` as const; export const HOST_ISOLATION_EXCEPTIONS_PATH = `${MANAGEMENT_PATH}/host_isolation_exceptions` as const; export const BLOCKLIST_PATH = `${MANAGEMENT_PATH}/blocklist` as const; -export const RESPONSE_ACTIONS_PATH = `${MANAGEMENT_PATH}/response_actions` as const; +export const ACTION_HISTORY_PATH = `${MANAGEMENT_PATH}/action_history` as const; export const ENTITY_ANALYTICS_PATH = '/entity_analytics' as const; export const APP_OVERVIEW_PATH = `${APP_PATH}${OVERVIEW_PATH}` as const; export const APP_LANDING_PATH = `${APP_PATH}${LANDING_PATH}` as const; @@ -183,7 +183,7 @@ export const APP_EVENT_FILTERS_PATH = `${APP_PATH}${EVENT_FILTERS_PATH}` as cons export const APP_HOST_ISOLATION_EXCEPTIONS_PATH = `${APP_PATH}${HOST_ISOLATION_EXCEPTIONS_PATH}` as const; export const APP_BLOCKLIST_PATH = `${APP_PATH}${BLOCKLIST_PATH}` as const; -export const APP_RESPONSE_ACTIONS_PATH = `${APP_PATH}${RESPONSE_ACTIONS_PATH}` as const; +export const APP_ACTION_HISTORY_PATH = `${APP_PATH}${ACTION_HISTORY_PATH}` as const; export const APP_ENTITY_ANALYTICS_PATH = `${APP_PATH}${ENTITY_ANALYTICS_PATH}` as const; // cloud logs to exclude from default index pattern @@ -458,3 +458,6 @@ export enum BulkActionsDryRunErrCode { MACHINE_LEARNING_AUTH = 'MACHINE_LEARNING_AUTH', MACHINE_LEARNING_INDEX_PATTERN = 'MACHINE_LEARNING_INDEX_PATTERN', } + +export const RISKY_HOSTS_DOC_LINK = + 'https://www.github.com/elastic/detection-rules/blob/main/docs/experimental-machine-learning/host-risk-score.md'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts index 46186479bf7268..08e609938938c6 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts @@ -284,7 +284,7 @@ const { patch: threatMatchPatchParams, response: threatMatchResponseParams, } = buildAPISchemas(threatMatchRuleParams); -export { threatMatchCreateParams }; +export { threatMatchCreateParams, threatMatchResponseParams }; const queryRuleParams = { required: { @@ -307,7 +307,7 @@ const { response: queryResponseParams, } = buildAPISchemas(queryRuleParams); -export { queryCreateParams }; +export { queryCreateParams, queryResponseParams }; const savedQueryRuleParams = { required: { @@ -332,7 +332,7 @@ const { response: savedQueryResponseParams, } = buildAPISchemas(savedQueryRuleParams); -export { savedQueryCreateParams }; +export { savedQueryCreateParams, savedQueryResponseParams }; const thresholdRuleParams = { required: { @@ -356,7 +356,7 @@ const { response: thresholdResponseParams, } = buildAPISchemas(thresholdRuleParams); -export { thresholdCreateParams }; +export { thresholdCreateParams, thresholdResponseParams }; const machineLearningRuleParams = { required: { @@ -373,7 +373,7 @@ const { response: machineLearningResponseParams, } = buildAPISchemas(machineLearningRuleParams); -export { machineLearningCreateParams }; +export { machineLearningCreateParams, machineLearningResponseParams }; const newTermsRuleParams = { required: { @@ -397,7 +397,7 @@ const { response: newTermsResponseParams, } = buildAPISchemas(newTermsRuleParams); -export { newTermsCreateParams }; +export { newTermsCreateParams, newTermsResponseParams }; // --------------------------------------- // END type specific parameter definitions @@ -503,14 +503,27 @@ const responseOptionalFields = { execution_summary: RuleExecutionSummary, }; -export const fullResponseSchema = t.intersection([ +const sharedResponseSchema = t.intersection([ baseResponseParams, - responseTypeSpecific, t.exact(t.type(responseRequiredFields)), t.exact(t.partial(responseOptionalFields)), ]); +export type SharedResponseSchema = t.TypeOf<typeof sharedResponseSchema>; +export const fullResponseSchema = t.intersection([sharedResponseSchema, responseTypeSpecific]); export type FullResponseSchema = t.TypeOf<typeof fullResponseSchema>; +// Convenience types for type specific responses +type ResponseSchema<T> = SharedResponseSchema & T; +export type EqlResponseSchema = ResponseSchema<t.TypeOf<typeof eqlResponseParams>>; +export type ThreatMatchResponseSchema = ResponseSchema<t.TypeOf<typeof threatMatchResponseParams>>; +export type QueryResponseSchema = ResponseSchema<t.TypeOf<typeof queryResponseParams>>; +export type SavedQueryResponseSchema = ResponseSchema<t.TypeOf<typeof savedQueryResponseParams>>; +export type ThresholdResponseSchema = ResponseSchema<t.TypeOf<typeof thresholdResponseParams>>; +export type MachineLearningResponseSchema = ResponseSchema< + t.TypeOf<typeof machineLearningResponseParams> +>; +export type NewTermsResponseSchema = ResponseSchema<t.TypeOf<typeof newTermsResponseParams>>; + export interface RulePreviewLogs { errors: string[]; warnings: string[]; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts index e12fbf29183029..5c934b0d2e0408 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts @@ -12,5 +12,3 @@ export * from './import_rules_schema'; export * from './prepackaged_rules_schema'; export * from './prepackaged_rules_status_schema'; export * from './rules_bulk_schema'; -export * from './rules_schema'; -export * from './type_timeline_only_schema'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.test.ts index 00800b94747168..69e31522ef40a2 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.test.ts @@ -10,12 +10,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import type { RulesBulkSchema } from './rules_bulk_schema'; import { rulesBulkSchema } from './rules_bulk_schema'; -import type { RulesSchema } from './rules_schema'; import type { ErrorSchema } from './error_schema'; import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getRulesSchemaMock } from './rules_schema.mocks'; import { getErrorSchemaMock } from './error_schema.mocks'; +import type { FullResponseSchema } from '../request'; describe('prepackaged_rule_schema', () => { test('it should validate a regular message and and error together with a uuid', () => { @@ -73,15 +73,14 @@ describe('prepackaged_rule_schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "type"', - 'Invalid value "undefined" supplied to "error"', - ]); + expect(getPaths(left(message.errors))).toContain( + 'Invalid value "undefined" supplied to "error"' + ); expect(message.schema).toEqual({}); }); test('it should NOT validate a type of "query" when it has extra data', () => { - const rule: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); + const rule: FullResponseSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); rule.invalid_extra_data = 'invalid_extra_data'; const payload: RulesBulkSchema = [rule]; const decoded = rulesBulkSchema.decode(payload); @@ -93,7 +92,7 @@ describe('prepackaged_rule_schema', () => { }); test('it should NOT validate a type of "query" when it has extra data next to a valid error', () => { - const rule: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); + const rule: FullResponseSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); rule.invalid_extra_data = 'invalid_extra_data'; const payload: RulesBulkSchema = [getErrorSchemaMock(), rule]; const decoded = rulesBulkSchema.decode(payload); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.ts index 57d812645ed385..65c55f356c44bf 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.ts @@ -7,8 +7,8 @@ import * as t from 'io-ts'; -import { rulesSchema } from './rules_schema'; +import { fullResponseSchema } from '../request'; import { errorSchema } from './error_schema'; -export const rulesBulkSchema = t.array(t.union([rulesSchema, errorSchema])); +export const rulesBulkSchema = t.array(t.union([fullResponseSchema, errorSchema])); export type RulesBulkSchema = t.TypeOf<typeof rulesBulkSchema>; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts index c3fbec8a6d7b31..bf6583a6855f08 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts @@ -6,13 +6,19 @@ */ import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../constants'; +import type { + EqlResponseSchema, + MachineLearningResponseSchema, + QueryResponseSchema, + SavedQueryResponseSchema, + SharedResponseSchema, + ThreatMatchResponseSchema, +} from '../request'; import { getListArrayMock } from '../types/lists.mock'; -import type { RulesSchema } from './rules_schema'; - export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; -export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => ({ +const getResponseBaseParams = (anchorDate: string = ANCHOR_DATE): SharedResponseSchema => ({ author: [], id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', created_at: new Date(anchorDate).toISOString(), @@ -24,45 +30,83 @@ export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchem from: 'now-6m', immutable: false, name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', references: ['test 1', 'test 2'], - severity: 'high', + severity: 'high' as const, severity_mapping: [], updated_by: 'elastic_kibana', tags: ['some fake tag 1', 'some fake tag 2'], to: 'now', - type: 'query', threat: [], version: 1, output_index: '.siem-signals-default', max_signals: 100, risk_score: 55, risk_score_mapping: [], - language: 'kuery', rule_id: 'query-rule-id', interval: '5m', exceptions_list: getListArrayMock(), related_integrations: [], required_fields: [], setup: '', + throttle: 'no_actions', + actions: [], + building_block_type: undefined, + note: undefined, + license: undefined, + outcome: undefined, + alias_target_id: undefined, + alias_purpose: undefined, + timeline_id: undefined, + timeline_title: undefined, + meta: undefined, + rule_name_override: undefined, + timestamp_override: undefined, + timestamp_override_fallback_disabled: undefined, + namespace: undefined, }); -export const getRulesMlSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => { - const basePayload = getRulesSchemaMock(anchorDate); - const { filters, index, query, language, ...rest } = basePayload; +export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): QueryResponseSchema => ({ + ...getResponseBaseParams(anchorDate), + query: 'user.name: root or user.name: admin', + type: 'query', + language: 'kuery', + index: undefined, + data_view_id: undefined, + filters: undefined, + saved_id: undefined, +}); +export const getSavedQuerySchemaMock = ( + anchorDate: string = ANCHOR_DATE +): SavedQueryResponseSchema => ({ + ...getResponseBaseParams(anchorDate), + query: 'user.name: root or user.name: admin', + type: 'saved_query', + saved_id: 'save id 123', + language: 'kuery', + index: undefined, + data_view_id: undefined, + filters: undefined, +}); +export const getRulesMlSchemaMock = ( + anchorDate: string = ANCHOR_DATE +): MachineLearningResponseSchema => { return { - ...rest, + ...getResponseBaseParams(anchorDate), type: 'machine_learning', anomaly_threshold: 59, machine_learning_job_id: 'some_machine_learning_job_id', }; }; -export const getThreatMatchingSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => { +export const getThreatMatchingSchemaMock = ( + anchorDate: string = ANCHOR_DATE +): ThreatMatchResponseSchema => { return { - ...getRulesSchemaMock(anchorDate), + ...getResponseBaseParams(anchorDate), type: 'threat_match', + query: 'user.name: root or user.name: admin', + language: 'kuery', threat_index: ['index-123'], threat_mapping: [{ entries: [{ field: 'host.name', type: 'mapping', value: 'host.name' }] }], threat_query: '*:*', @@ -84,6 +128,14 @@ export const getThreatMatchingSchemaMock = (anchorDate: string = ANCHOR_DATE): R }, }, ], + index: undefined, + data_view_id: undefined, + filters: undefined, + saved_id: undefined, + threat_indicator_path: undefined, + threat_language: undefined, + concurrent_searches: undefined, + items_per_search: undefined, }; }; @@ -91,7 +143,9 @@ export const getThreatMatchingSchemaMock = (anchorDate: string = ANCHOR_DATE): R * Useful for e2e backend tests where it doesn't have date time and other * server side properties attached to it. */ -export const getThreatMatchingSchemaPartialMock = (enabled = false): Partial<RulesSchema> => { +export const getThreatMatchingSchemaPartialMock = ( + enabled = false +): Partial<ThreatMatchResponseSchema> => { return { author: [], created_by: 'elastic', @@ -160,11 +214,17 @@ export const getThreatMatchingSchemaPartialMock = (enabled = false): Partial<Rul }; }; -export const getRulesEqlSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => { +export const getRulesEqlSchemaMock = (anchorDate: string = ANCHOR_DATE): EqlResponseSchema => { return { - ...getRulesSchemaMock(anchorDate), + ...getResponseBaseParams(anchorDate), language: 'eql', type: 'eql', query: 'process where true', + index: undefined, + data_view_id: undefined, + filters: undefined, + timestamp_field: undefined, + event_category_override: undefined, + tiebreaker_field: undefined, }; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts index bac55c85109291..0a337eb28bc1c8 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts @@ -7,35 +7,23 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import type * as t from 'io-ts'; -import type { RulesSchema } from './rules_schema'; -import { - rulesSchema, - checkTypeDependents, - getDependents, - addSavedId, - addQueryFields, - addTimelineTitle, - addMlFields, - addThreatMatchFields, - addEqlFields, -} from './rules_schema'; import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; -import type { TypeAndTimelineOnly } from './type_timeline_only_schema'; import { getRulesSchemaMock, getRulesMlSchemaMock, + getSavedQuerySchemaMock, getThreatMatchingSchemaMock, getRulesEqlSchemaMock, } from './rules_schema.mocks'; -import type { ListArray } from '@kbn/securitysolution-io-ts-list-types'; +import { fullResponseSchema } from '../request'; +import type { FullResponseSchema } from '../request'; describe('rules_schema', () => { test('it should validate a type of "query" without anything extra', () => { const payload = getRulesSchemaMock(); - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); const expected = getRulesSchemaMock(); @@ -45,10 +33,10 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "query" when it has extra data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); + const payload: FullResponseSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); payload.invalid_extra_data = 'invalid_extra_data'; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -57,55 +45,48 @@ describe('rules_schema', () => { }); test('it should NOT validate invalid_data for the type', () => { - const payload: Omit<RulesSchema, 'type'> & { type: string } = getRulesSchemaMock(); + const payload: Omit<FullResponseSchema, 'type'> & { type: string } = getRulesSchemaMock(); payload.type = 'invalid_data'; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "invalid_data" supplied to "type"', - ]); + expect(getPaths(left(message.errors))).toHaveLength(1); expect(message.schema).toEqual({}); }); - test('it should NOT validate a type of "query" with a saved_id together', () => { - const payload = getRulesSchemaMock(); + test('it should validate a type of "query" with a saved_id together', () => { + const payload: FullResponseSchema & { saved_id?: string } = getRulesSchemaMock(); payload.type = 'query'; payload.saved_id = 'save id 123'; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "saved_id"']); - expect(message.schema).toEqual({}); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); }); test('it should validate a type of "saved_query" with a "saved_id" dependent', () => { - const payload = getRulesSchemaMock(); - payload.type = 'saved_query'; - payload.saved_id = 'save id 123'; + const payload = getSavedQuerySchemaMock(); - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - const expected = getRulesSchemaMock(); - - expected.type = 'saved_query'; - expected.saved_id = 'save id 123'; + const expected = getSavedQuerySchemaMock(); expect(getPaths(left(message.errors))).toEqual([]); expect(message.schema).toEqual(expected); }); test('it should NOT validate a type of "saved_query" without a "saved_id" dependent', () => { - const payload = getRulesSchemaMock(); - payload.type = 'saved_query'; + const payload: FullResponseSchema & { saved_id?: string } = getSavedQuerySchemaMock(); + // @ts-expect-error delete payload.saved_id; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -116,12 +97,11 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "saved_query" when it has extra data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); - payload.type = 'saved_query'; - payload.saved_id = 'save id 123'; + const payload: FullResponseSchema & { saved_id?: string; invalid_extra_data?: string } = + getSavedQuerySchemaMock(); payload.invalid_extra_data = 'invalid_extra_data'; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -134,7 +114,7 @@ describe('rules_schema', () => { payload.timeline_id = 'some timeline id'; payload.timeline_title = 'some timeline title'; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); const expected = getRulesSchemaMock(); @@ -146,12 +126,12 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "timeline_id" if there is "timeline_title" dependent when it has extra invalid data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); + const payload: FullResponseSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); payload.timeline_id = 'some timeline id'; payload.timeline_title = 'some timeline title'; payload.invalid_extra_data = 'invalid_extra_data'; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -159,575 +139,11 @@ describe('rules_schema', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate a type of "timeline_id" if there is NOT a "timeline_title" dependent', () => { - const payload = getRulesSchemaMock(); - payload.timeline_id = 'some timeline id'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "timeline_title"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "timeline_title" if there is NOT a "timeline_id" dependent', () => { - const payload = getRulesSchemaMock(); - payload.timeline_title = 'some timeline title'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "timeline_title"']); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_title" but there is NOT a "timeline_id"', () => { - const payload = getRulesSchemaMock(); - payload.saved_id = 'some saved id'; - payload.type = 'saved_query'; - payload.timeline_title = 'some timeline title'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "timeline_title"']); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_id" but there is NOT a "timeline_title"', () => { - const payload = getRulesSchemaMock(); - payload.saved_id = 'some saved id'; - payload.type = 'saved_query'; - payload.timeline_id = 'some timeline id'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "timeline_title"', - ]); - expect(message.schema).toEqual({}); - }); - - describe('checkTypeDependents', () => { - test('it should validate a type of "query" without anything extra', () => { - const payload = getRulesSchemaMock(); - - const decoded = checkTypeDependents(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getRulesSchemaMock(); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - - test('it should NOT validate invalid_data for the type', () => { - const payload: Omit<RulesSchema, 'type'> & { type: string } = getRulesSchemaMock(); - payload.type = 'invalid_data'; - - const decoded = checkTypeDependents(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "invalid_data" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "query" with a saved_id together', () => { - const payload = getRulesSchemaMock(); - payload.type = 'query'; - payload.saved_id = 'save id 123'; - - const decoded = checkTypeDependents(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "saved_id"']); - expect(message.schema).toEqual({}); - }); - - test('it should validate a type of "saved_query" with a "saved_id" dependent', () => { - const payload = getRulesSchemaMock(); - payload.type = 'saved_query'; - payload.saved_id = 'save id 123'; - - const decoded = checkTypeDependents(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getRulesSchemaMock(); - - expected.type = 'saved_query'; - expected.saved_id = 'save id 123'; - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - - test('it should NOT validate a type of "saved_query" without a "saved_id" dependent', () => { - const payload = getRulesSchemaMock(); - payload.type = 'saved_query'; - delete payload.saved_id; - - const decoded = checkTypeDependents(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "saved_id"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "saved_query" when it has extra data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); - payload.type = 'saved_query'; - payload.saved_id = 'save id 123'; - payload.invalid_extra_data = 'invalid_extra_data'; - - const decoded = checkTypeDependents(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); - expect(message.schema).toEqual({}); - }); - - test('it should validate a type of "timeline_id" if there is a "timeline_title" dependent', () => { - const payload = getRulesSchemaMock(); - payload.timeline_id = 'some timeline id'; - payload.timeline_title = 'some timeline title'; - - const decoded = checkTypeDependents(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getRulesSchemaMock(); - expected.timeline_id = 'some timeline id'; - expected.timeline_title = 'some timeline title'; - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - - test('it should NOT validate a type of "timeline_id" if there is "timeline_title" dependent when it has extra invalid data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); - payload.timeline_id = 'some timeline id'; - payload.timeline_title = 'some timeline title'; - payload.invalid_extra_data = 'invalid_extra_data'; - - const decoded = checkTypeDependents(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "timeline_id" if there is NOT a "timeline_title" dependent', () => { - const payload = getRulesSchemaMock(); - payload.timeline_id = 'some timeline id'; - - const decoded = checkTypeDependents(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "timeline_title"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "timeline_title" if there is NOT a "timeline_id" dependent', () => { - const payload = getRulesSchemaMock(); - payload.timeline_title = 'some timeline title'; - - const decoded = checkTypeDependents(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "timeline_title"']); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_title" but there is NOT a "timeline_id"', () => { - const payload = getRulesSchemaMock(); - payload.saved_id = 'some saved id'; - payload.type = 'saved_query'; - payload.timeline_title = 'some timeline title'; - - const decoded = checkTypeDependents(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "timeline_title"']); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_id" but there is NOT a "timeline_title"', () => { - const payload = getRulesSchemaMock(); - payload.saved_id = 'some saved id'; - payload.type = 'saved_query'; - payload.timeline_id = 'some timeline id'; - - const decoded = checkTypeDependents(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "timeline_title"', - ]); - expect(message.schema).toEqual({}); - }); - }); - - describe('getDependents', () => { - test('it should validate a type of "query" without anything extra', () => { - const payload = getRulesSchemaMock(); - - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getRulesSchemaMock(); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - - test('it should validate a namespace as string', () => { - const payload = { - ...getRulesSchemaMock(), - namespace: 'a namespace', - }; - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should NOT validate invalid_data for the type', () => { - const payload: Omit<RulesSchema, 'type'> & { type: string } = getRulesSchemaMock(); - payload.type = 'invalid_data'; - - const dependents = getDependents(payload as unknown as TypeAndTimelineOnly); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "invalid_data" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "query" with a saved_id together', () => { - const payload = getRulesSchemaMock(); - payload.type = 'query'; - payload.saved_id = 'save id 123'; - - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "saved_id"']); - expect(message.schema).toEqual({}); - }); - - test('it should validate a type of "saved_query" with a "saved_id" dependent', () => { - const payload = getRulesSchemaMock(); - payload.type = 'saved_query'; - payload.saved_id = 'save id 123'; - - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getRulesSchemaMock(); - - expected.type = 'saved_query'; - expected.saved_id = 'save id 123'; - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - - test('it should NOT validate a type of "saved_query" without a "saved_id" dependent', () => { - const payload = getRulesSchemaMock(); - payload.type = 'saved_query'; - delete payload.saved_id; - - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "saved_id"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "saved_query" when it has extra data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); - payload.type = 'saved_query'; - payload.saved_id = 'save id 123'; - payload.invalid_extra_data = 'invalid_extra_data'; - - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); - expect(message.schema).toEqual({}); - }); - - test('it should validate a type of "timeline_id" if there is a "timeline_title" dependent', () => { - const payload = getRulesSchemaMock(); - payload.timeline_id = 'some timeline id'; - payload.timeline_title = 'some timeline title'; - - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getRulesSchemaMock(); - expected.timeline_id = 'some timeline id'; - expected.timeline_title = 'some timeline title'; - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - - test('it should NOT validate a type of "timeline_id" if there is "timeline_title" dependent when it has extra invalid data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); - payload.timeline_id = 'some timeline id'; - payload.timeline_title = 'some timeline title'; - payload.invalid_extra_data = 'invalid_extra_data'; - - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "timeline_id" if there is NOT a "timeline_title" dependent', () => { - const payload = getRulesSchemaMock(); - payload.timeline_id = 'some timeline id'; - - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "timeline_title"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "timeline_title" if there is NOT a "timeline_id" dependent', () => { - const payload = getRulesSchemaMock(); - payload.timeline_title = 'some timeline title'; - - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "timeline_title"']); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_title" but there is NOT a "timeline_id"', () => { - const payload = getRulesSchemaMock(); - payload.saved_id = 'some saved id'; - payload.type = 'saved_query'; - payload.timeline_title = 'some timeline title'; - - const decoded = checkTypeDependents(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "timeline_title"']); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_id" but there is NOT a "timeline_title"', () => { - const payload = getRulesSchemaMock(); - payload.saved_id = 'some saved id'; - payload.type = 'saved_query'; - payload.timeline_id = 'some timeline id'; - - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "timeline_title"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it validates an ML rule response', () => { - const payload = getRulesMlSchemaMock(); - - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getRulesMlSchemaMock(); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - - test('it rejects a response with both ML and query properties', () => { - const payload = { - ...getRulesSchemaMock(), - ...getRulesMlSchemaMock(), - }; - - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "query,language"']); - expect(message.schema).toEqual({}); - }); - - test('it validates a threat_match response', () => { - const payload = getThreatMatchingSchemaMock(); - - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getThreatMatchingSchemaMock(); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - - test('it rejects a response with threat_match properties but type of "query"', () => { - const payload: RulesSchema = { - ...getThreatMatchingSchemaMock(), - type: 'query', - }; - - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'invalid keys "threat_index,["index-123"],threat_mapping,[{"entries":[{"field":"host.name","type":"mapping","value":"host.name"}]}],threat_query,threat_filters,[{"bool":{"must":[{"query_string":{"query":"host.name: linux","analyze_wildcard":true,"time_zone":"Zulu"}}],"filter":[],"should":[],"must_not":[]}}]"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it validates an eql rule response', () => { - const payload = getRulesEqlSchemaMock(); - - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getRulesEqlSchemaMock(); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - }); - - describe('addSavedId', () => { - test('should return empty array if not given a type of "saved_query"', () => { - const emptyArray = addSavedId({ type: 'query' }); - const expected: t.Mixed[] = []; - expect(emptyArray).toEqual(expected); - }); - - test('should array of size 2 given a "saved_query"', () => { - const array = addSavedId({ type: 'saved_query' }); - expect(array.length).toEqual(2); - }); - }); - - describe('addTimelineTitle', () => { - test('should return empty array if not given a timeline_id', () => { - const emptyArray = addTimelineTitle({ type: 'query' }); - const expected: t.Mixed[] = []; - expect(emptyArray).toEqual(expected); - }); - - test('should array of size 2 given a "timeline_id" that is not null', () => { - const array = addTimelineTitle({ type: 'query', timeline_id: 'some id' }); - expect(array.length).toEqual(2); - }); - }); - - describe('addQueryFields', () => { - test('should return empty array if type is not "query"', () => { - const fields = addQueryFields({ type: 'machine_learning' }); - const expected: t.Mixed[] = []; - expect(fields).toEqual(expected); - }); - - test('should return two fields for a rule of type "query"', () => { - const fields = addQueryFields({ type: 'query' }); - expect(fields.length).toEqual(3); - }); - - test('should return two fields for a rule of type "threshold"', () => { - const fields = addQueryFields({ type: 'threshold' }); - expect(fields.length).toEqual(3); - }); - - test('should return two fields for a rule of type "saved_query"', () => { - const fields = addQueryFields({ type: 'saved_query' }); - expect(fields.length).toEqual(3); - }); - - test('should return two fields for a rule of type "threat_match"', () => { - const fields = addQueryFields({ type: 'threat_match' }); - expect(fields.length).toEqual(3); - }); - }); - - describe('addMlFields', () => { - test('should return empty array if type is not "machine_learning"', () => { - const fields = addMlFields({ type: 'query' }); - const expected: t.Mixed[] = []; - expect(fields).toEqual(expected); - }); - - test('should return two fields for a rule of type "machine_learning"', () => { - const fields = addMlFields({ type: 'machine_learning' }); - expect(fields.length).toEqual(2); - }); - }); - describe('exceptions_list', () => { test('it should validate an empty array for "exceptions_list"', () => { const payload = getRulesSchemaMock(); payload.exceptions_list = []; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); const expected = getRulesSchemaMock(); @@ -737,11 +153,11 @@ describe('rules_schema', () => { }); test('it should NOT validate when "exceptions_list" is not expected type', () => { - const payload: Omit<RulesSchema, 'exceptions_list'> & { + const payload: Omit<FullResponseSchema, 'exceptions_list'> & { exceptions_list?: string; } = { ...getRulesSchemaMock(), exceptions_list: 'invalid_data' }; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -750,53 +166,13 @@ describe('rules_schema', () => { ]); expect(message.schema).toEqual({}); }); - - test('it should default to empty array if "exceptions_list" is undefined ', () => { - const payload: Omit<RulesSchema, 'exceptions_list'> & { - exceptions_list?: ListArray; - } = getRulesSchemaMock(); - payload.exceptions_list = undefined; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...payload, exceptions_list: [] }); - }); - }); - - describe('addThreatMatchFields', () => { - test('should return empty array if type is not "threat_match"', () => { - const fields = addThreatMatchFields({ type: 'query' }); - const expected: t.Mixed[] = []; - expect(fields).toEqual(expected); - }); - - test('should return nine (9) fields for a rule of type "threat_match"', () => { - const fields = addThreatMatchFields({ type: 'threat_match' }); - expect(fields.length).toEqual(10); - }); - }); - - describe('addEqlFields', () => { - test('should return empty array if type is not "eql"', () => { - const fields = addEqlFields({ type: 'query' }); - const expected: t.Mixed[] = []; - expect(fields).toEqual(expected); - }); - - test('should return 3 fields for a rule of type "eql"', () => { - const fields = addEqlFields({ type: 'eql' }); - expect(fields.length).toEqual(6); - }); }); describe('data_view_id', () => { test('it should validate a type of "query" with "data_view_id" defined', () => { const payload = { ...getRulesSchemaMock(), data_view_id: 'logs-*' }; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); const expected = { ...getRulesSchemaMock(), data_view_id: 'logs-*' }; @@ -806,18 +182,16 @@ describe('rules_schema', () => { }); test('it should validate a type of "saved_query" with "data_view_id" defined', () => { - const payload = getRulesSchemaMock(); - payload.type = 'saved_query'; - payload.saved_id = 'save id 123'; + const payload: FullResponseSchema & { saved_id?: string; data_view_id?: string } = + getSavedQuerySchemaMock(); payload.data_view_id = 'logs-*'; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - const expected = getRulesSchemaMock(); + const expected: FullResponseSchema & { saved_id?: string; data_view_id?: string } = + getSavedQuerySchemaMock(); - expected.type = 'saved_query'; - expected.saved_id = 'save id 123'; expected.data_view_id = 'logs-*'; expect(getPaths(left(message.errors))).toEqual([]); @@ -827,8 +201,7 @@ describe('rules_schema', () => { test('it should validate a type of "eql" with "data_view_id" defined', () => { const payload = { ...getRulesEqlSchemaMock(), data_view_id: 'logs-*' }; - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); const expected = { ...getRulesEqlSchemaMock(), data_view_id: 'logs-*' }; @@ -840,8 +213,7 @@ describe('rules_schema', () => { test('it should validate a type of "threat_match" with "data_view_id" defined', () => { const payload = { ...getThreatMatchingSchemaMock(), data_view_id: 'logs-*' }; - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); const expected = { ...getThreatMatchingSchemaMock(), data_view_id: 'logs-*' }; @@ -853,8 +225,7 @@ describe('rules_schema', () => { test('it should NOT validate a type of "machine_learning" with "data_view_id" defined', () => { const payload = { ...getRulesMlSchemaMock(), data_view_id: 'logs-*' }; - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts deleted file mode 100644 index 794ef71bf05362..00000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts +++ /dev/null @@ -1,366 +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 * as t from 'io-ts'; -import { isObject } from 'lodash/fp'; -import type { Either } from 'fp-ts/lib/Either'; -import { left, fold } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; - -import { - actions, - from, - machine_learning_job_id, - risk_score, - DefaultRiskScoreMappingArray, - DefaultSeverityMappingArray, - threat_index, - concurrent_searches, - items_per_search, - threat_query, - threat_filters, - threat_mapping, - threat_language, - threat_indicator_path, - threats, - type, - language, - severity, - throttle, - max_signals, -} from '@kbn/securitysolution-io-ts-alerting-types'; -import { DefaultStringArray, version } from '@kbn/securitysolution-io-ts-types'; -import { DefaultListArray } from '@kbn/securitysolution-io-ts-list-types'; - -import { isMlRule } from '../../../machine_learning/helpers'; -import { isThresholdRule } from '../../utils'; -import { RuleExecutionSummary } from '../../rule_monitoring'; -import { - anomaly_threshold, - data_view_id, - description, - enabled, - timestamp_field, - event_category_override, - tiebreaker_field, - false_positives, - id, - immutable, - index, - interval, - rule_id, - name, - output_index, - query, - references, - updated_by, - tags, - to, - created_at, - created_by, - updated_at, - saved_id, - timeline_id, - timeline_title, - threshold, - filters, - meta, - outcome, - alias_target_id, - alias_purpose, - note, - building_block_type, - license, - rule_name_override, - timestamp_override, - namespace, - RelatedIntegrationArray, - RequiredFieldArray, - SetupGuide, -} from '../common'; - -import type { TypeAndTimelineOnly } from './type_timeline_only_schema'; -import { typeAndTimelineOnlySchema } from './type_timeline_only_schema'; - -/** - * This is the required fields for the rules schema response. Put all required properties on - * this base for schemas such as create_rules, update_rules, for the correct validation of the - * output schema. - */ -export const requiredRulesSchema = t.type({ - author: DefaultStringArray, - description, - enabled, - false_positives, - from, - id, - immutable, - interval, - rule_id, - output_index, - max_signals, - risk_score, - risk_score_mapping: DefaultRiskScoreMappingArray, - name, - references, - severity, - severity_mapping: DefaultSeverityMappingArray, - updated_by, - tags, - to, - type, - threat: threats, - created_at, - updated_at, - created_by, - version, - exceptions_list: DefaultListArray, - related_integrations: RelatedIntegrationArray, - required_fields: RequiredFieldArray, - setup: SetupGuide, -}); - -export type RequiredRulesSchema = t.TypeOf<typeof requiredRulesSchema>; - -/** - * If you have type dependents or exclusive or situations add them here AND update the - * check_type_dependents file for whichever REST flow it is going through. - */ -export const dependentRulesSchema = t.partial({ - // All but ML - data_view_id, - - // query fields - language, - query, - - // eql only fields - timestamp_field, - event_category_override, - tiebreaker_field, - - // when type = saved_query, saved_id is required - saved_id, - - // These two are required together or not at all. - timeline_id, - timeline_title, - - // ML fields - anomaly_threshold, - machine_learning_job_id, - - // Threshold fields - threshold, - - // Threat Match fields - threat_filters, - threat_index, - threat_query, - concurrent_searches, - items_per_search, - threat_mapping, - threat_language, - threat_indicator_path, -}); - -/** - * This is the partial or optional fields for the rules schema. Put all optional - * properties on this. DO NOT PUT type dependents such as xor relationships here. - * Instead use dependentRulesSchema and check_type_dependents for how to do those. - */ -export const partialRulesSchema = t.partial({ - actions, - building_block_type, - license, - throttle, - rule_name_override, - timestamp_override, - filters, - meta, - outcome, - alias_target_id, - alias_purpose, - index, - namespace, - note, - uuid: id, // Move to 'required' post-migration - execution_summary: RuleExecutionSummary, -}); - -/** - * This is the rules schema WITHOUT typeDependents. You don't normally want to use this for a decode - */ -export const rulesWithoutTypeDependentsSchema = t.intersection([ - t.exact(dependentRulesSchema), - t.exact(partialRulesSchema), - t.exact(requiredRulesSchema), -]); -export type RulesWithoutTypeDependentsSchema = t.TypeOf<typeof rulesWithoutTypeDependentsSchema>; - -/** - * This is the rulesSchema you want to use for checking type dependents and all the properties - * through: rulesSchema.decode(someJSONObject) - */ -export const rulesSchema = new t.Type< - RulesWithoutTypeDependentsSchema, - RulesWithoutTypeDependentsSchema, - unknown ->( - 'RulesSchema', - (input: unknown): input is RulesWithoutTypeDependentsSchema => isObject(input), - (input): Either<t.Errors, RulesWithoutTypeDependentsSchema> => { - return checkTypeDependents(input); - }, - t.identity -); - -/** - * This is the correct type you want to use for Rules that are outputted from the - * REST interface. This has all base and all optional properties merged together. - */ -export type RulesSchema = t.TypeOf<typeof rulesSchema>; - -export const addSavedId = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (typeAndTimelineOnly.type === 'saved_query') { - return [ - t.exact(t.type({ saved_id: dependentRulesSchema.props.saved_id })), - t.exact(t.partial({ data_view_id: dependentRulesSchema.props.data_view_id })), - ]; - } else { - return []; - } -}; - -export const addTimelineTitle = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (typeAndTimelineOnly.timeline_id != null) { - return [ - t.exact(t.type({ timeline_title: dependentRulesSchema.props.timeline_title })), - t.exact(t.type({ timeline_id: dependentRulesSchema.props.timeline_id })), - ]; - } else { - return []; - } -}; - -export const addQueryFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (['query', 'saved_query', 'threshold', 'threat_match'].includes(typeAndTimelineOnly.type)) { - return [ - t.exact(t.type({ query: dependentRulesSchema.props.query })), - t.exact(t.type({ language: dependentRulesSchema.props.language })), - t.exact(t.partial({ data_view_id: dependentRulesSchema.props.data_view_id })), - ]; - } else { - return []; - } -}; - -export const addMlFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (isMlRule(typeAndTimelineOnly.type)) { - return [ - t.exact(t.type({ anomaly_threshold: dependentRulesSchema.props.anomaly_threshold })), - t.exact( - t.type({ machine_learning_job_id: dependentRulesSchema.props.machine_learning_job_id }) - ), - ]; - } else { - return []; - } -}; - -export const addThresholdFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (isThresholdRule(typeAndTimelineOnly.type)) { - return [ - t.exact(t.type({ threshold: dependentRulesSchema.props.threshold })), - t.exact(t.partial({ saved_id: dependentRulesSchema.props.saved_id })), - t.exact(t.partial({ data_view_id: dependentRulesSchema.props.data_view_id })), - ]; - } else { - return []; - } -}; - -export const addEqlFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (typeAndTimelineOnly.type === 'eql') { - return [ - t.exact(t.partial({ timestamp_field: dependentRulesSchema.props.timestamp_field })), - t.exact( - t.partial({ event_category_override: dependentRulesSchema.props.event_category_override }) - ), - t.exact(t.partial({ tiebreaker_field: dependentRulesSchema.props.tiebreaker_field })), - t.exact(t.type({ query: dependentRulesSchema.props.query })), - t.exact(t.type({ language: dependentRulesSchema.props.language })), - t.exact(t.partial({ data_view_id: dependentRulesSchema.props.data_view_id })), - ]; - } else { - return []; - } -}; - -export const addThreatMatchFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (typeAndTimelineOnly.type === 'threat_match') { - return [ - t.exact(t.partial({ data_view_id: dependentRulesSchema.props.data_view_id })), - t.exact(t.type({ threat_query: dependentRulesSchema.props.threat_query })), - t.exact(t.type({ threat_index: dependentRulesSchema.props.threat_index })), - t.exact(t.type({ threat_mapping: dependentRulesSchema.props.threat_mapping })), - t.exact(t.partial({ threat_language: dependentRulesSchema.props.threat_language })), - t.exact(t.partial({ threat_filters: dependentRulesSchema.props.threat_filters })), - t.exact( - t.partial({ threat_indicator_path: dependentRulesSchema.props.threat_indicator_path }) - ), - t.exact(t.partial({ saved_id: dependentRulesSchema.props.saved_id })), - t.exact(t.partial({ concurrent_searches: dependentRulesSchema.props.concurrent_searches })), - t.exact( - t.partial({ - items_per_search: dependentRulesSchema.props.items_per_search, - }) - ), - ]; - } else { - return []; - } -}; - -export const getDependents = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed => { - const dependents: t.Mixed[] = [ - t.exact(requiredRulesSchema), - t.exact(partialRulesSchema), - ...addSavedId(typeAndTimelineOnly), - ...addTimelineTitle(typeAndTimelineOnly), - ...addQueryFields(typeAndTimelineOnly), - ...addMlFields(typeAndTimelineOnly), - ...addThresholdFields(typeAndTimelineOnly), - ...addEqlFields(typeAndTimelineOnly), - ...addThreatMatchFields(typeAndTimelineOnly), - ]; - - if (dependents.length > 1) { - // This unsafe cast is because t.intersection does not use an array but rather a set of - // tuples and really does not look like they expected us to ever dynamically build up - // intersections, but here we are doing that. Looking at their code, although they limit - // the array elements to 5, it looks like you have N number of intersections - const unsafeCast: [t.Mixed, t.Mixed] = dependents as [t.Mixed, t.Mixed]; - return t.intersection(unsafeCast); - } else { - // We are not allowed to call t.intersection with a single value so we return without - // it here normally. - return dependents[0]; - } -}; - -export const checkTypeDependents = (input: unknown): Either<t.Errors, RequiredRulesSchema> => { - const typeOnlyDecoded = typeAndTimelineOnlySchema.decode(input); - const onLeft = (errors: t.Errors): Either<t.Errors, RequiredRulesSchema> => left(errors); - const onRight = ( - typeAndTimelineOnly: TypeAndTimelineOnly - ): Either<t.Errors, RequiredRulesSchema> => { - const intersections = getDependents(typeAndTimelineOnly); - return intersections.decode(input); - }; - return pipe(typeOnlyDecoded, fold(onLeft, onRight)); -}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.test.ts deleted file mode 100644 index 8026d99713214e..00000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.test.ts +++ /dev/null @@ -1,66 +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 { left } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; - -import type { TypeAndTimelineOnly } from './type_timeline_only_schema'; -import { typeAndTimelineOnlySchema } from './type_timeline_only_schema'; -import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; - -describe('prepackaged_rule_schema', () => { - test('it should validate a a type and timeline_id together', () => { - const payload: TypeAndTimelineOnly = { - type: 'query', - timeline_id: 'some id', - }; - const decoded = typeAndTimelineOnlySchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate just a type without a timeline_id of type query', () => { - const payload: TypeAndTimelineOnly = { - type: 'query', - }; - const decoded = typeAndTimelineOnlySchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate just a type of saved_query', () => { - const payload: TypeAndTimelineOnly = { - type: 'saved_query', - }; - const decoded = typeAndTimelineOnlySchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should NOT validate an invalid type', () => { - const payload: Omit<TypeAndTimelineOnly, 'type'> & { type: string } = { - type: 'some other type', - }; - const decoded = typeAndTimelineOnlySchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some other type" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.ts deleted file mode 100644 index b164ab9b44e4fd..00000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.ts +++ /dev/null @@ -1,21 +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 * as t from 'io-ts'; - -import { type } from '@kbn/securitysolution-io-ts-alerting-types'; -import { timeline_id } from '../common/schemas'; - -/** - * Special schema type that is only the type and the timeline_id. - * This is used for dependent type checking only. - */ -export const typeAndTimelineOnlySchema = t.intersection([ - t.exact(t.type({ type })), - t.exact(t.partial({ timeline_id })), -]); -export type TypeAndTimelineOnly = t.TypeOf<typeof typeAndTimelineOnlySchema>; diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts index 8c917b4ef6898f..868129f3a67374 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts @@ -167,7 +167,9 @@ export class BaseDataGenerator<GeneratedDoc extends {} = {}> { } protected randomVersion(): string { - return [7, ...this.randomNGenerator(20, 2)].map((x) => x.toString()).join('.'); + // the `major` is sometimes (30%) 7 and most of the time (70%) 8 + const major = this.randomBoolean(0.4) ? 7 : 8; + return [major, ...this.randomNGenerator(20, 2)].map((x) => x.toString()).join('.'); } protected randomChoice<T>(choices: T[] | readonly T[]): T { diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts new file mode 100644 index 00000000000000..67ff2d3605093b --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts @@ -0,0 +1,149 @@ +/* + * 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 { DeepPartial } from 'utility-types'; +import { merge } from 'lodash'; +import { gte } from 'semver'; +import { BaseDataGenerator } from './base_data_generator'; +import type { HostMetadataInterface, OSFields } from '../types'; +import { EndpointStatus, HostPolicyResponseActionStatus } from '../types'; + +/** + * Metadata generator for docs that are sent by the Endpoint running on hosts + */ +export class EndpointMetadataGenerator extends BaseDataGenerator { + /** Generate an Endpoint host metadata document */ + generate(overrides: DeepPartial<HostMetadataInterface> = {}): HostMetadataInterface { + const ts = overrides['@timestamp'] ?? new Date().getTime(); + const hostName = this.randomHostname(); + const agentVersion = overrides?.agent?.version ?? this.randomVersion(); + const agentId = this.seededUUIDv4(); + const isIsolated = this.randomBoolean(0.3); + const capabilities = ['isolation']; + + // v8.4 introduced additional endpoint capabilities + if (gte(agentVersion, '8.4.0')) { + capabilities.push('kill_process', 'suspend_process', 'running_processes'); + } + + const hostMetadataDoc: HostMetadataInterface = { + '@timestamp': ts, + event: { + created: ts, + id: this.seededUUIDv4(), + kind: 'metric', + category: ['host'], + type: ['info'], + module: 'endpoint', + action: 'endpoint_metadata', + dataset: 'endpoint.metadata', + }, + data_stream: { + type: 'metrics', + dataset: 'endpoint.metadata', + namespace: 'default', + }, + agent: { + version: agentVersion, + id: agentId, + type: 'endpoint', + }, + elastic: { + agent: { + id: agentId, + }, + }, + host: { + id: this.seededUUIDv4(), + hostname: hostName, + name: hostName, + architecture: this.randomString(10), + ip: this.randomArray(3, () => this.randomIP()), + mac: this.randomArray(3, () => this.randomMac()), + os: this.randomOsFields(), + }, + Endpoint: { + status: EndpointStatus.enrolled, + policy: { + applied: { + name: 'With Eventing', + id: 'C2A9093E-E289-4C0A-AA44-8C32A414FA7A', + status: HostPolicyResponseActionStatus.success, + endpoint_policy_version: 3, + version: 5, + }, + }, + configuration: { + isolation: isIsolated, + }, + state: { + isolation: isIsolated, + }, + capabilities, + }, + }; + + return merge(hostMetadataDoc, overrides); + } + + protected randomOsFields(): OSFields { + return this.randomChoice([ + { + name: 'Windows', + full: 'Windows 10', + version: '10.0', + platform: 'Windows', + family: 'windows', + Ext: { + variant: 'Windows Pro', + }, + }, + { + name: 'Windows', + full: 'Windows Server 2016', + version: '10.0', + platform: 'Windows', + family: 'windows', + Ext: { + variant: 'Windows Server', + }, + }, + { + name: 'Windows', + full: 'Windows Server 2012', + version: '6.2', + platform: 'Windows', + family: 'windows', + Ext: { + variant: 'Windows Server', + }, + }, + { + name: 'Windows', + full: 'Windows Server 2012R2', + version: '6.3', + platform: 'Windows', + family: 'windows', + Ext: { + variant: 'Windows Server Release 2', + }, + }, + { + Ext: { + variant: 'Debian', + }, + kernel: '4.19.0-21-cloud-amd64 #1 SMP Debian 4.19.249-2 (2022-06-30)', + name: 'Linux', + family: 'debian', + type: 'linux', + version: '10.12', + platform: 'debian', + full: 'Debian 10.12', + }, + ]); + } +} diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts index 1586e47aa1f4c9..a005004a906820 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts @@ -504,7 +504,7 @@ describe('data generator', () => { events[previousProcessEventIndex].process?.parent?.entity_id ); expect(events[events.length - 1].event?.kind).toEqual('alert'); - expect(events[events.length - 1].event?.category).toEqual('malware'); + expect(events[events.length - 1].event?.category).toEqual('behavior'); }); }); diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index c9b554fc89031d..59808b1df430c1 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -14,17 +14,17 @@ import type { KibanaAssetReference, } from '@kbn/fleet-plugin/common'; import { agentPolicyStatuses } from '@kbn/fleet-plugin/common'; +import { EndpointMetadataGenerator } from './data_generators/endpoint_metadata_generator'; import type { AlertEvent, DataStream, - Host, HostMetadata, + HostMetadataInterface, HostPolicyResponse, - OSFields, PolicyData, SafeEndpointEvent, } from './types'; -import { EndpointStatus, HostPolicyResponseActionStatus } from './types'; +import { HostPolicyResponseActionStatus } from './types'; import { policyFactory } from './models/policy_config'; import { ancestryArray, @@ -49,55 +49,6 @@ export type Event = AlertEvent | SafeEndpointEvent; */ export const ANCESTRY_LIMIT: number = 2; -const Windows: OSFields[] = [ - { - name: 'Windows', - full: 'Windows 10', - version: '10.0', - platform: 'Windows', - family: 'windows', - Ext: { - variant: 'Windows Pro', - }, - }, - { - name: 'Windows', - full: 'Windows Server 2016', - version: '10.0', - platform: 'Windows', - family: 'windows', - Ext: { - variant: 'Windows Server', - }, - }, - { - name: 'Windows', - full: 'Windows Server 2012', - version: '6.2', - platform: 'Windows', - family: 'windows', - Ext: { - variant: 'Windows Server', - }, - }, - { - name: 'Windows', - full: 'Windows Server 2012R2', - version: '6.3', - platform: 'Windows', - family: 'windows', - Ext: { - variant: 'Windows Server Release 2', - }, - }, -]; - -const Linux: OSFields[] = []; - -const Mac: OSFields[] = []; - -const OS: OSFields[] = [...Windows, ...Mac, ...Linux]; - const POLICY_RESPONSE_STATUSES: HostPolicyResponseActionStatus[] = [ HostPolicyResponseActionStatus.success, HostPolicyResponseActionStatus.failure, @@ -105,13 +56,7 @@ const POLICY_RESPONSE_STATUSES: HostPolicyResponseActionStatus[] = [ HostPolicyResponseActionStatus.unsupported, ]; -const APPLIED_POLICIES: Array<{ - name: string; - id: string; - status: HostPolicyResponseActionStatus; - endpoint_policy_version: number; - version: number; -}> = [ +const APPLIED_POLICIES: Array<HostMetadataInterface['Endpoint']['policy']['applied']> = [ { name: 'Default', id: '00000000-0000-0000-0000-000000000000', @@ -231,38 +176,7 @@ const OTHER_EVENT_CATEGORIES: Record< }, }; -interface HostInfo { - elastic: { - agent: { - id: string; - }; - }; - agent: { - version: string; - id: string; - type: string; - }; - host: Host; - Endpoint: { - status: EndpointStatus; - policy: { - applied: { - id: string; - status: HostPolicyResponseActionStatus; - name: string; - endpoint_policy_version: number; - version: number; - }; - }; - configuration?: { - isolation: boolean; - }; - state?: { - isolation: boolean; - }; - capabilities?: string[]; - }; -} +type CommonHostInfo = Pick<HostMetadataInterface, 'elastic' | 'agent' | 'host' | 'Endpoint'>; interface NodeState { event: Event; @@ -403,17 +317,32 @@ const alertsDefaultDataStream = { namespace: 'default', }; +/** + * Generator to create various ElasticSearch documents that are normally streamed by the Endpoint. + * + * NOTE: this generator currently reuses certain data (ex. `this.commonInfo`) across several + * documents, thus use caution if manipulating/mutating value in the generated data + * (ex. in tests). Individual standalone generators exist, whose generated data does not + * contain shared data structures. + */ export class EndpointDocGenerator extends BaseDataGenerator { - commonInfo: HostInfo; + commonInfo: CommonHostInfo; sequence: number = 0; + private readonly metadataGenerator: EndpointMetadataGenerator; + /** * The EndpointDocGenerator parameters * * @param seed either a string to seed the random number generator or a random number generator function + * @param MetadataGenerator */ - constructor(seed: string | seedrandom.prng = Math.random().toString()) { + constructor( + seed: string | seedrandom.prng = Math.random().toString(), + MetadataGenerator: typeof EndpointMetadataGenerator = EndpointMetadataGenerator + ) { super(seed); + this.metadataGenerator = new MetadataGenerator(seed); this.commonInfo = this.createHostData(); } @@ -456,47 +385,12 @@ export class EndpointDocGenerator extends BaseDataGenerator { }; } - private createHostData(): HostInfo { - const hostName = this.randomHostname(); - const isIsolated = this.randomBoolean(0.3); - const agentVersion = this.randomVersion(); - const capabilities = ['isolation', 'kill_process', 'suspend_process', 'running_processes']; - const agentId = this.seededUUIDv4(); + private createHostData(): CommonHostInfo { + const { agent, elastic, host, Endpoint } = this.metadataGenerator.generate({ + Endpoint: { policy: { applied: this.randomChoice(APPLIED_POLICIES) } }, + }); - return { - agent: { - version: agentVersion, - id: agentId, - type: 'endpoint', - }, - elastic: { - agent: { - id: agentId, - }, - }, - host: { - id: this.seededUUIDv4(), - hostname: hostName, - name: hostName, - architecture: this.randomString(10), - ip: this.randomArray(3, () => this.randomIP()), - mac: this.randomArray(3, () => this.randomMac()), - os: this.randomChoice(OS), - }, - Endpoint: { - status: EndpointStatus.enrolled, - policy: { - applied: this.randomChoice(APPLIED_POLICIES), - }, - configuration: { - isolation: isIsolated, - }, - state: { - isolation: isIsolated, - }, - capabilities, - }, - }; + return { agent, elastic, host, Endpoint }; } /** @@ -508,21 +402,11 @@ export class EndpointDocGenerator extends BaseDataGenerator { ts = new Date().getTime(), metadataDataStream = metadataDefaultDataStream ): HostMetadata { - return { + return this.metadataGenerator.generate({ '@timestamp': ts, - event: { - created: ts, - id: this.seededUUIDv4(), - kind: 'metric', - category: ['host'], - type: ['info'], - module: 'endpoint', - action: 'endpoint_metadata', - dataset: 'endpoint.metadata', - }, - ...this.commonInfo, data_stream: metadataDataStream, - }; + ...this.commonInfo, + }); } /** @@ -1628,7 +1512,7 @@ export class EndpointDocGenerator extends BaseDataGenerator { } /** - * Generates an Ingest `package policy` that includes the Endpoint Policy data + * Generates a Fleet `package policy` that includes the Endpoint Policy data */ public generatePolicyPackagePolicy(): PolicyData { const created = new Date(Date.now() - 8.64e7).toISOString(); // 24h ago @@ -1673,7 +1557,7 @@ export class EndpointDocGenerator extends BaseDataGenerator { } /** - * Generate an Agent Policy (ingest) + * Generate an Agent Policy (Fleet) */ public generateAgentPolicy(): GetAgentPoliciesResponseItem { // FIXME: remove and use new FleetPackagePolicyGenerator (#2262) @@ -1693,7 +1577,7 @@ export class EndpointDocGenerator extends BaseDataGenerator { } /** - * Generate an EPM Package for Endpoint + * Generate a Fleet EPM Package for Endpoint */ public generateEpmPackage(): GetPackagesResponse['items'][0] { return { diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts index ea01e62fbc8077..4971dc83c29aa1 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -43,6 +43,7 @@ export type IndexedHostsAndAlertsResponse = IndexedHostsResponse; * @param alertsPerHost * @param fleet * @param options + * @param DocGenerator */ export async function indexHostsAndAlerts( client: Client, @@ -56,7 +57,8 @@ export async function indexHostsAndAlerts( alertIndex: string, alertsPerHost: number, fleet: boolean, - options: TreeOptions = {} + options: TreeOptions = {}, + DocGenerator: typeof EndpointDocGenerator = EndpointDocGenerator ): Promise<IndexedHostsAndAlertsResponse> { const random = seedrandom(seed); const epmEndpointPackage = await getEndpointPackageInfo(kbnClient); @@ -91,7 +93,7 @@ export async function indexHostsAndAlerts( const realPolicies: Record<string, CreatePackagePolicyResponse['item']> = {}; for (let i = 0; i < numHosts; i++) { - const generator = new EndpointDocGenerator(random); + const generator = new DocGenerator(random); const indexedHosts = await indexEndpointHostDocs({ numDocs, client, diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 0f791a1f409d1f..cdadf9619f0087 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -492,9 +492,11 @@ export type HostInfo = Immutable<{ }; }>; -// HostMetadataDetails is now just HostMetadata -// HostDetails is also just HostMetadata -export type HostMetadata = Immutable<{ +// Host metadata document streamed up to ES by the Endpoint running on host machines. +// NOTE: `HostMetadata` type is the original and defined as Immutable. If needing to +// work with metadata that is not mutable, use `HostMetadataInterface` +export type HostMetadata = Immutable<HostMetadataInterface>; +export interface HostMetadataInterface { '@timestamp': number; event: { created: number; @@ -542,10 +544,11 @@ export type HostMetadata = Immutable<{ agent: { id: string; version: string; + type: string; }; host: Host; data_stream: DataStream; -}>; +} export type UnitedAgentMetadata = Immutable<{ agent: { diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/risk_score/all/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/risk_score/all/index.ts index 2eee56f15f0834..bbb2991551f269 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/risk_score/all/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/risk_score/all/index.ts @@ -25,33 +25,35 @@ export interface RiskScoreRequestOptions extends IEsSearchRequest { export interface HostsRiskScoreStrategyResponse extends IEsSearchResponse { inspect?: Maybe<Inspect>; totalCount: number; - data: HostsRiskScore[] | undefined; + data: HostRiskScore[] | undefined; } export interface UsersRiskScoreStrategyResponse extends IEsSearchResponse { inspect?: Maybe<Inspect>; totalCount: number; - data: UsersRiskScore[] | undefined; + data: UserRiskScore[] | undefined; } -export interface RiskScore { - '@timestamp': string; - risk: string; - risk_stats: { - rule_risks: RuleRisk[]; - risk_score: number; - }; +export interface RiskStats { + rule_risks: RuleRisk[]; + calculated_score_norm: number; + multipliers: string[]; + calculated_level: RiskSeverity; } -export interface HostsRiskScore extends RiskScore { +export interface HostRiskScore { + '@timestamp': string; host: { name: string; + risk: RiskStats; }; } -export interface UsersRiskScore extends RiskScore { +export interface UserRiskScore { + '@timestamp': string; user: { name: string; + risk: RiskStats; }; } @@ -66,17 +68,23 @@ export type RiskScoreSortField = SortField<RiskScoreFields>; export const enum RiskScoreFields { timestamp = '@timestamp', hostName = 'host.name', + hostRiskScore = 'host.risk.calculated_score_norm', + hostRisk = 'host.risk.calculated_level', userName = 'user.name', - riskScore = 'risk_stats.risk_score', - risk = 'risk', + userRiskScore = 'user.risk.calculated_score_norm', + userRisk = 'user.risk.calculated_level', } export interface RiskScoreItem { _id?: Maybe<string>; [RiskScoreFields.hostName]: Maybe<string>; [RiskScoreFields.userName]: Maybe<string>; - [RiskScoreFields.risk]: Maybe<RiskSeverity>; - [RiskScoreFields.riskScore]: Maybe<number>; + + [RiskScoreFields.hostRisk]: Maybe<RiskSeverity>; + [RiskScoreFields.userRisk]: Maybe<RiskSeverity>; + + [RiskScoreFields.hostRiskScore]: Maybe<number>; + [RiskScoreFields.userRiskScore]: Maybe<number>; } export const enum RiskSeverity { @@ -86,3 +94,6 @@ export const enum RiskSeverity { high = 'High', critical = 'Critical', } + +export const isUserRiskScore = (risk: HostRiskScore | UserRiskScore): risk is UserRiskScore => + 'user' in risk; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/risk_score/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/risk_score/common/index.ts index c6f651440edb93..b7ef7a4c5cbda3 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/risk_score/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/risk_score/common/index.ts @@ -33,4 +33,7 @@ export enum RiskQueries { kpiRiskScore = 'kpiRiskScore', } -export type RiskScoreAggByFields = 'host.name' | 'user.name'; +export const enum RiskScoreEntity { + host = 'host', + user = 'user', +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/risk_score/kpi/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/risk_score/kpi/index.ts index 2fe24f44400881..4d95846a4f7402 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/risk_score/kpi/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/risk_score/kpi/index.ts @@ -6,7 +6,7 @@ */ import type { IEsSearchRequest, IEsSearchResponse } from '@kbn/data-plugin/common'; -import type { FactoryQueryTypes, RiskScoreAggByFields, RiskSeverity } from '../..'; +import type { FactoryQueryTypes, RiskScoreEntity, RiskSeverity } from '../..'; import type { ESQuery } from '../../../../typed_json'; import type { Inspect, Maybe } from '../../../common'; @@ -15,7 +15,7 @@ export interface KpiRiskScoreRequestOptions extends IEsSearchRequest { defaultIndex: string[]; factoryQueryType?: FactoryQueryTypes; filterQuery?: ESQuery | string | undefined; - aggBy: RiskScoreAggByFields; + entity: RiskScoreEntity; } export interface KpiRiskScoreStrategyResponse extends IEsSearchResponse { diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/common/index.ts index c5cb4351757a0a..81ef6daf184a8b 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/common/index.ts @@ -5,22 +5,15 @@ * 2.0. */ -import type { CommonFields, Maybe, RiskSeverity, SortField } from '../../..'; +import type { CommonFields, Maybe, RiskScoreFields, RiskSeverity, SortField } from '../../..'; import type { HostEcs } from '../../../../ecs/host'; import type { UserEcs } from '../../../../ecs/user'; -export const enum UserRiskScoreFields { - timestamp = '@timestamp', - userName = 'user.name', - riskScore = 'risk_stats.risk_score', - risk = 'risk', -} - export interface UserRiskScoreItem { _id?: Maybe<string>; - [UserRiskScoreFields.userName]: Maybe<string>; - [UserRiskScoreFields.risk]: Maybe<RiskSeverity>; - [UserRiskScoreFields.riskScore]: Maybe<number>; + [RiskScoreFields.userName]: Maybe<string>; + [RiskScoreFields.userRisk]: Maybe<RiskSeverity>; + [RiskScoreFields.userRiskScore]: Maybe<number>; } export interface UserItem { diff --git a/x-pack/plugins/security_solution/cypress/README.md b/x-pack/plugins/security_solution/cypress/README.md index 44f1fa63d732b1..5124e8d5c06858 100644 --- a/x-pack/plugins/security_solution/cypress/README.md +++ b/x-pack/plugins/security_solution/cypress/README.md @@ -83,7 +83,7 @@ This configuration runs cypress tests against an arbitrary host. #### integration-test (CI) -This configuration is driven by [elastic/integration-test](https://github.com/elastic/integration-test) which, as part of a bigger set of tests, provisions one VM with two instances configured in CCS mode and runs the [CCS Cypress test specs](./ccs_integration). +This configuration is driven by [elastic/integration-test](https://github.com/elastic/integration-test) which, as part of a bigger set of tests, provisions one VM with two instances configured in CCS mode and runs the [CCS Cypress test specs](./ccs_e2e). The two clusters are named `admin` and `data` and are reachable as follows: @@ -280,13 +280,13 @@ If you are debugging a flaky test, a good tip is to insert a `cy.wait(<some long Below you can find the folder structure used on our Cypress tests. -### ccs_integration/ +### ccs_e2e/ Contains the specs that are executed in a Cross Cluster Search configuration. -### integration/ +### e2e/ -Cypress convention. Contains the specs that are going to be executed. +Cypress convention starting version 10 (previously known as integration). Contains the specs that are going to be executed. ### fixtures/ diff --git a/x-pack/plugins/security_solution/cypress/ccs_integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/ccs_e2e/detection_alerts/alerts_details.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/ccs_integration/detection_alerts/alerts_details.spec.ts rename to x-pack/plugins/security_solution/cypress/ccs_e2e/detection_alerts/alerts_details.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/ccs_integration/detection_rules/event_correlation_rule.spec.ts b/x-pack/plugins/security_solution/cypress/ccs_e2e/detection_rules/event_correlation_rule.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/ccs_integration/detection_rules/event_correlation_rule.spec.ts rename to x-pack/plugins/security_solution/cypress/ccs_e2e/detection_rules/event_correlation_rule.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/cypress.config.ts b/x-pack/plugins/security_solution/cypress/cypress.config.ts new file mode 100644 index 00000000000000..8361a89fef8265 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/cypress.config.ts @@ -0,0 +1,30 @@ +/* + * 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 { defineConfig } from 'cypress'; + +// eslint-disable-next-line import/no-default-export +export default defineConfig({ + defaultCommandTimeout: 20000, + execTimeout: 20000, + pageLoadTimeout: 20000, + screenshotsFolder: '../../../target/kibana-security-solution/cypress/screenshots', + trashAssetsBeforeRuns: false, + video: false, + videosFolder: '../../../target/kibana-security-solution/cypress/videos', + viewportHeight: 946, + viewportWidth: 1680, + e2e: { + baseUrl: 'http://localhost:5601', + // We've imported your old cypress plugins here. + // You may want to clean this up later by importing these. + setupNodeEvents(on, config) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require('./plugins')(on, config); + }, + }, +}); diff --git a/x-pack/plugins/security_solution/cypress/cypress.json b/x-pack/plugins/security_solution/cypress/cypress.json deleted file mode 100644 index ab350ecad739fc..00000000000000 --- a/x-pack/plugins/security_solution/cypress/cypress.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "defaultCommandTimeout": 20000, - "execTimeout": 20000, - "pageLoadTimeout": 20000, - "nodeVersion": "system", - "screenshotsFolder": "../../../target/kibana-security-solution/cypress/screenshots", - "trashAssetsBeforeRuns": false, - "video": false, - "videosFolder": "../../../target/kibana-security-solution/cypress/videos", - "viewportHeight": 946, - "viewportWidth": 1680 -} diff --git a/x-pack/plugins/security_solution/cypress/cypress_ci.config.ts b/x-pack/plugins/security_solution/cypress/cypress_ci.config.ts new file mode 100644 index 00000000000000..c9f16520a1b079 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/cypress_ci.config.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { defineConfig } from 'cypress'; + +// eslint-disable-next-line import/no-default-export +export default defineConfig({ + defaultCommandTimeout: 120000, + execTimeout: 120000, + pageLoadTimeout: 120000, + numTestsKeptInMemory: 0, + retries: { + runMode: 2, + }, + screenshotsFolder: '../../../target/kibana-security-solution/cypress/screenshots', + trashAssetsBeforeRuns: false, + video: false, + videosFolder: '../../../target/kibana-security-solution/cypress/videos', + viewportHeight: 946, + viewportWidth: 1680, + e2e: { + baseUrl: 'http://localhost:5601', + // We've imported your old cypress plugins here. + // You may want to clean this up later by importing these. + setupNodeEvents(on, config) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require('./plugins')(on, config); + }, + }, +}); diff --git a/x-pack/plugins/security_solution/cypress/cypress_ci.json b/x-pack/plugins/security_solution/cypress/cypress_ci.json deleted file mode 100644 index 36ba5eb10b26c6..00000000000000 --- a/x-pack/plugins/security_solution/cypress/cypress_ci.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "defaultCommandTimeout": 120000, - "execTimeout": 120000, - "pageLoadTimeout": 120000, - "nodeVersion": "system", - "numTestsKeptInMemory": 0, - "retries": { - "runMode": 2 - }, - "screenshotsFolder": "../../../target/kibana-security-solution/cypress/screenshots", - "trashAssetsBeforeRuns": false, - "video": false, - "videosFolder": "../../../target/kibana-security-solution/cypress/videos", - "viewportHeight": 946, - "viewportWidth": 1680 -} diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/attach_alert_to_case.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/cases/attach_alert_to_case.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/cases/attach_alert_to_case.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/cases/attach_alert_to_case.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/cases/attach_timeline.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/cases/attach_timeline.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/cases/connector_options.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/cases/connector_options.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/cases/connectors.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/cases/connectors.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/cases/creation.cy.ts similarity index 96% rename from x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/cases/creation.cy.ts index 0b53557dbcd036..81ae0d5b40fd75 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/cases/creation.cy.ts @@ -17,8 +17,6 @@ import { ALL_CASES_OPEN_CASES_STATS, ALL_CASES_OPENED_ON, ALL_CASES_PAGE_TITLE, - ALL_CASES_REPORTER, - ALL_CASES_REPORTERS_COUNT, ALL_CASES_SERVICE_NOW_INCIDENT, ALL_CASES_TAGS, ALL_CASES_TAGS_COUNT, @@ -85,10 +83,8 @@ describe('Cases', () => { cy.get(ALL_CASES_CLOSED_CASES_STATS).should('have.text', '0'); cy.get(ALL_CASES_IN_PROGRESS_CASES_STATS).should('have.text', '0'); cy.get(ALL_CASES_OPEN_CASES_COUNT).should('have.text', 'Open (1)'); - cy.get(ALL_CASES_REPORTERS_COUNT).should('have.text', 'Reporter1'); cy.get(ALL_CASES_TAGS_COUNT).should('have.text', 'Tags2'); cy.get(ALL_CASES_NAME).should('have.text', this.mycase.name); - cy.get(ALL_CASES_REPORTER).should('have.text', 'e'); (this.mycase as TestCase).tags.forEach((tag) => { cy.get(ALL_CASES_TAGS(tag)).should('have.text', tag); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/cases/privileges.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/cases/privileges.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/dashboards/entity_analytics.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/dashboards/entity_analytics.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/dashboards/entity_analytics.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/dashboards/entity_analytics.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/data_sources/create_runtime_field.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/data_sources/create_runtime_field.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/data_sources/create_runtime_field.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/data_sources/create_runtime_field.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/data_sources/sourcerer.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/data_sources/sourcerer.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alert_flyout.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alert_flyout.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_alerts/alert_flyout.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alert_flyout.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_details.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_details.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_detection_callouts_index_outdated.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_detection_callouts_index_outdated.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_detection_callouts_index_outdated.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_detection_callouts_index_outdated.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/building_block_alerts.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/building_block_alerts.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_alerts/building_block_alerts.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_alerts/building_block_alerts.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/changing_alert_status.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/changing_alert_status.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_alerts/changing_alert_status.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_alerts/changing_alert_status.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/cti_enrichments.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_alerts/cti_enrichments.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/investigate_in_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/investigate_in_timeline.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_alerts/investigate_in_timeline.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_alerts/investigate_in_timeline.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/missing_privileges_callout.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/missing_privileges_callout.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_alerts/missing_privileges_callout.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_alerts/missing_privileges_callout.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_response/open_alerts_in_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_response/open_alerts_in_timeline.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_response/open_alerts_in_timeline.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_response/open_alerts_in_timeline.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/all_rules_read_only.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/all_rules_read_only.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_rules/all_rules_read_only.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_rules/all_rules_read_only.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/bulk_edit_rules.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_rules/bulk_edit_rules.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts similarity index 99% rename from x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts index 5725e511f2dec3..bdc76cc5f0a595 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts @@ -278,6 +278,7 @@ describe('Custom query rules', () => { deleteRuleFromDetailsPage(); + // @ts-expect-error update types cy.waitFor('@deleteRule').then(() => { cy.get(RULES_TABLE).should('exist'); cy.get(RULES_TABLE) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule_data_view.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule_data_view.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule_data_view.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule_data_view.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/event_correlation_rule.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_rules/event_correlation_rule.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/export_rule.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_rules/export_rule.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/import_rules.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/import_rules.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_rules/import_rules.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_rules/import_rules.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/indicator_match_rule.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_rules/indicator_match_rule.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/links.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/links.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_rules/links.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_rules/links.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/machine_learning_rule.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_rules/machine_learning_rule.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/new_terms_rule.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/new_terms_rule.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_rules/new_terms_rule.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_rules/new_terms_rule.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/override.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_rules/override.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/prebuilt_rules.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_rules/prebuilt_rules.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/related_integrations.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/related_integrations.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_rules/related_integrations.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_rules/related_integrations.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/rules_selection.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rules_selection.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_rules/rules_selection.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_rules/rules_selection.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/rules_table_auto_refresh.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rules_table_auto_refresh.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_rules/rules_table_auto_refresh.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_rules/rules_table_auto_refresh.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/sorting.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_rules/sorting.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/threshold_rule.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/detection_rules/threshold_rule.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_table_flow/add_exception.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/add_exception.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_table_flow/add_exception.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/add_exception.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/exceptions_flyout.cy.ts similarity index 98% rename from x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/exceptions/exceptions_flyout.cy.ts index dfb018b4bfb5ad..20f55a4fffd4fa 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/exceptions/exceptions_flyout.cy.ts @@ -303,7 +303,7 @@ describe('Exceptions flyout', () => { goToExceptionsTab(); }); - context('When updating an item with version conflict', () => { + context.skip('When updating an item with version conflict', () => { it('Displays version conflict error', () => { editException(); @@ -334,7 +334,7 @@ describe('Exceptions flyout', () => { }); }); - context('When updating an item for a list that has since been deleted', () => { + context.skip('When updating an item for a list that has since been deleted', () => { it('Displays missing exception list error', () => { editException(); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_management_flow/all_exception_lists_read_only.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/exceptions_management_flow/all_exception_lists_read_only.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_management_flow/all_exception_lists_read_only.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/exceptions/exceptions_management_flow/all_exception_lists_read_only.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/exceptions_table.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/exceptions/exceptions_table.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/add_exception.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/add_exception.spec.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/add_exception.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/add_exception.spec.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/add_exception_data_view.spect.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/add_exception_data_view.spect.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/add_exception_data_view.spect.ts rename to x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/add_exception_data_view.spect.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/edit_exception.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/edit_exception.spec.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/edit_exception.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/edit_exception.spec.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/edit_exception_data_view.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/edit_exception_data_view.spec.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/edit_exception_data_view.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/edit_exception_data_view.spec.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/read_only_view.spect.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/read_only_view.spect.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/read_only_view.spect.ts rename to x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/read_only_view.spect.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/filters/pinned_filters.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/filters/pinned_filters.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/filters/pinned_filters.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/filters/pinned_filters.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/guided_onboarding/tour.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/guided_onboarding/tour.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/guided_onboarding/tour.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/guided_onboarding/tour.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/header/navigation.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/header/navigation.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/header/navigation.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/header/navigation.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/header/search_bar.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/header/search_bar.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/header/search_bar.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/header/search_bar.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/host_details/risk_tab.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/host_details/risk_tab.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/host_details/risk_tab.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/host_details/risk_tab.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/hosts/events_viewer.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/hosts/events_viewer.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/hosts/host_risk_tab.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/hosts/host_risk_tab.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/hosts/host_risk_tab.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/hosts/host_risk_tab.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/hosts/hosts_risk_column.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/hosts/hosts_risk_column.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/hosts/hosts_risk_column.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/hosts/hosts_risk_column.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/hosts/inspect.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/hosts/inspect.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/hosts/inspect.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/hosts/inspect.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/ml/ml_conditional_links.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/ml/ml_conditional_links.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/ml/ml_conditional_links.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/ml/ml_conditional_links.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/network/hover_actions.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/network/hover_actions.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/network/hover_actions.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/network/hover_actions.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/network/inspect.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/network/inspect.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/network/inspect.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/network/inspect.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/network/overflow_items.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/network/overflow_items.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/network/overflow_items.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/network/overflow_items.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/overview/cti_link_panel.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/overview/cti_link_panel.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/overview/cti_link_panel.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/overview/cti_link_panel.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/overview/overview.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/overview/overview.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/pagination/pagination.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/pagination/pagination.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/pagination/pagination.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/pagination/pagination.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/timeline_templates/creation.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/timeline_templates/creation.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_templates/export.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/timeline_templates/export.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/timeline_templates/export.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/timeline_templates/export.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/timelines/creation.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/timelines/creation.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/timelines/data_providers.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/timelines/data_providers.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/export.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/timelines/export.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/timelines/export.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/timelines/export.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/timelines/fields_browser.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/timelines/fields_browser.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/timelines/flyout_button.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/timelines/flyout_button.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/full_screen.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/timelines/full_screen.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/timelines/full_screen.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/timelines/full_screen.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/inspect.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/timelines/inspect.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/timelines/inspect.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/timelines/inspect.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/local_storage.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/timelines/local_storage.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/timelines/local_storage.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/timelines/local_storage.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/timelines/notes_tab.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/timelines/notes_tab.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/open_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/timelines/open_timeline.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/timelines/open_timeline.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/timelines/open_timeline.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/overview.tsx b/x-pack/plugins/security_solution/cypress/e2e/timelines/overview.cy.tsx similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/timelines/overview.tsx rename to x-pack/plugins/security_solution/cypress/e2e/timelines/overview.cy.tsx diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/timelines/pagination.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/timelines/pagination.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/timelines/query_tab.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/timelines/query_tab.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/timelines/row_renderers.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/timelines/row_renderers.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/timelines/search_or_filter.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/timelines/search_or_filter.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/toggle_column.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/timelines/toggle_column.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/timelines/toggle_column.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/timelines/toggle_column.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/urls/compatibility.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/urls/compatibility.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/urls/compatibility.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/urls/compatibility.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/urls/not_found.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/urls/not_found.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/urls/not_found.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/urls/not_found.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/urls/state.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/urls/state.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/users/inspect.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/users/inspect.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/users/inspect.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/users/inspect.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/users/user_details.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/users/user_details.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/users/user_details.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/users/user_details.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/users/users_tabs.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/users/users_tabs.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/users/users_tabs.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/users/users_tabs.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/value_lists/value_lists.spec.ts b/x-pack/plugins/security_solution/cypress/e2e/value_lists/value_lists.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/value_lists/value_lists.spec.ts rename to x-pack/plugins/security_solution/cypress/e2e/value_lists/value_lists.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/overview/risky_hosts_panel.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview/risky_hosts_panel.spec.ts deleted file mode 100644 index 686acd5fd048cf..00000000000000 --- a/x-pack/plugins/security_solution/cypress/integration/overview/risky_hosts_panel.spec.ts +++ /dev/null @@ -1,80 +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 { - OVERVIEW_RISKY_HOSTS_ENABLE_MODULE_BUTTON, - OVERVIEW_RISKY_HOSTS_LINKS, - OVERVIEW_RISKY_HOSTS_LINKS_ERROR_INNER_PANEL, - OVERVIEW_RISKY_HOSTS_LINKS_WARNING_INNER_PANEL, - OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT, - OVERVIEW_RISKY_HOSTS_DOC_LINK, - OVERVIEW_RISKY_HOSTS_IMPORT_DASHBOARD_BUTTON, -} from '../../screens/overview'; - -import { login, visit } from '../../tasks/login'; -import { OVERVIEW_URL } from '../../urls/navigation'; -import { cleanKibana } from '../../tasks/common'; -import { changeSpace } from '../../tasks/kibana_navigation'; -import { createSpace, removeSpace } from '../../tasks/api_calls/spaces'; -import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; - -const testSpaceName = 'test'; - -describe('Risky Hosts Link Panel', () => { - before(() => { - cleanKibana(); - login(); - }); - - it('renders disabled panel view as expected', () => { - visit(OVERVIEW_URL); - cy.get(`${OVERVIEW_RISKY_HOSTS_LINKS} ${OVERVIEW_RISKY_HOSTS_LINKS_ERROR_INNER_PANEL}`).should( - 'exist' - ); - cy.get(`${OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 0 hosts'); - cy.get(`${OVERVIEW_RISKY_HOSTS_ENABLE_MODULE_BUTTON}`).should('exist'); - cy.get(`${OVERVIEW_RISKY_HOSTS_DOC_LINK}`) - .should('have.attr', 'href') - .and('match', /host-risk-score.md/); - }); - - describe('enabled module', () => { - before(() => { - esArchiverLoad('risky_hosts'); - createSpace(testSpaceName); - }); - - after(() => { - esArchiverUnload('risky_hosts'); - removeSpace(testSpaceName); - }); - - it('renders disabled dashboard module as expected when there are no hosts in the selected time period', () => { - visit( - `${OVERVIEW_URL}?sourcerer=(timerange:(from:%272021-07-08T04:00:00.000Z%27,kind:absolute,to:%272021-07-09T03:59:59.999Z%27))` - ); - cy.get( - `${OVERVIEW_RISKY_HOSTS_LINKS} ${OVERVIEW_RISKY_HOSTS_LINKS_WARNING_INNER_PANEL}` - ).should('exist'); - cy.get(`${OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 0 hosts'); - }); - - it('renders space aware dashboard module as expected when there are hosts in the selected time period', () => { - visit(OVERVIEW_URL); - cy.get( - `${OVERVIEW_RISKY_HOSTS_LINKS} ${OVERVIEW_RISKY_HOSTS_LINKS_WARNING_INNER_PANEL}` - ).should('not.exist'); - cy.get(`${OVERVIEW_RISKY_HOSTS_IMPORT_DASHBOARD_BUTTON}`).should('exist'); - cy.get(`${OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 6 hosts'); - - changeSpace(testSpaceName); - cy.visit(`/s/${testSpaceName}${OVERVIEW_URL}`); - cy.get(`${OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 0 hosts'); - cy.get(`${OVERVIEW_RISKY_HOSTS_ENABLE_MODULE_BUTTON}`).should('exist'); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 0a5d5170473fb3..6b0051f12bc298 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -5,11 +5,11 @@ * 2.0. */ -import type { RulesSchema } from '../../common/detection_engine/schemas/response'; import { rawRules } from '../../server/lib/detection_engine/rules/prepackaged_rules'; import { getMockThreatData } from '../../public/detections/mitre/mitre_tactics_techniques'; import type { CompleteTimeline } from './timeline'; import { getTimeline, getIndicatorMatchTimelineTemplate } from './timeline'; +import type { FullResponseSchema } from '../../common/detection_engine/schemas/request'; export const totalNumberOfPrebuiltRules = rawRules.length; @@ -488,7 +488,9 @@ export const getEditedRule = (): CustomRule => ({ tags: [...getExistingRule().tags, 'edited'], }); -export const expectedExportedRule = (ruleResponse: Cypress.Response<RulesSchema>): string => { +export const expectedExportedRule = ( + ruleResponse: Cypress.Response<FullResponseSchema> +): string => { const { id, updated_at: updatedAt, @@ -498,14 +500,20 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response<RulesSchema> name, risk_score: riskScore, severity, - query, tags, timeline_id: timelineId, timeline_title: timelineTitle, } = ruleResponse.body; + let query: string | undefined; + if (ruleResponse.body.type === 'query') { + query = ruleResponse.body.query; + } + // NOTE: Order of the properties in this object matters for the tests to work. - const rule: RulesSchema = { + // TODO: Follow up https://github.com/elastic/kibana/pull/137628 and add an explicit type to this object + // without using Partial + const rule: Partial<FullResponseSchema> = { id, updated_at: updatedAt, updated_by: updatedBy, diff --git a/x-pack/plugins/security_solution/cypress/screens/all_cases.ts b/x-pack/plugins/security_solution/cypress/screens/all_cases.ts index 3f5bcb912ee442..8109bc31e07ada 100644 --- a/x-pack/plugins/security_solution/cypress/screens/all_cases.ts +++ b/x-pack/plugins/security_solution/cypress/screens/all_cases.ts @@ -34,11 +34,6 @@ export const ALL_CASES_OPENED_ON = '[data-test-subj="case-table-column-createdAt export const ALL_CASES_PAGE_TITLE = '[data-test-subj="header-page-title"]'; -export const ALL_CASES_REPORTER = '[data-test-subj="case-table-column-createdBy"]'; - -export const ALL_CASES_REPORTERS_COUNT = - '[data-test-subj="options-filter-popover-button-Reporter"]'; - export const ALL_CASES_SERVICE_NOW_INCIDENT = '[data-test-subj="case-table-column-external-notPushed"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index e80e8c210c7c99..4bf39a8bf1d797 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -170,7 +170,9 @@ export const RISK_OVERRIDE = export const RULES_CREATION_FORM = '[data-test-subj="stepDefineRule"]'; -export const RULES_CREATION_PREVIEW = '[data-test-subj="rule-preview"]'; +export const RULES_CREATION_PREVIEW_BUTTON = '[data-test-subj="preview-flyout"]'; + +export const RULES_CREATION_PREVIEW_REFRESH_BUTTON = '[data-test-subj="previewSubmitButton"]'; export const RULE_DESCRIPTION_INPUT = '[data-test-subj="detectionEngineStepAboutRuleDescription"] [data-test-subj="input"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/overview.ts b/x-pack/plugins/security_solution/cypress/screens/overview.ts index 1e91e4fe462b95..14bfce599dfaf5 100644 --- a/x-pack/plugins/security_solution/cypress/screens/overview.ts +++ b/x-pack/plugins/security_solution/cypress/screens/overview.ts @@ -151,19 +151,4 @@ export const OVERVIEW_CTI_LINKS_ERROR_INNER_PANEL = '[data-test-subj="cti-inner- export const OVERVIEW_CTI_TOTAL_EVENT_COUNT = `${OVERVIEW_CTI_LINKS} [data-test-subj="header-panel-subtitle"]`; export const OVERVIEW_CTI_ENABLE_MODULE_BUTTON = '[data-test-subj="cti-enable-module-button"]'; -export const OVERVIEW_RISKY_HOSTS_LINKS = '[data-test-subj="risky-hosts-dashboard-links"]'; -export const OVERVIEW_RISKY_HOSTS_LINKS_ERROR_INNER_PANEL = - '[data-test-subj="risky-hosts-inner-panel-danger"]'; -export const OVERVIEW_RISKY_HOSTS_LINKS_WARNING_INNER_PANEL = - '[data-test-subj="risky-hosts-inner-panel-warning"]'; -export const OVERVIEW_RISKY_HOSTS_VIEW_DASHBOARD_BUTTON = - '[data-test-subj="risky-hosts-view-dashboard-button"]'; -export const OVERVIEW_RISKY_HOSTS_IMPORT_DASHBOARD_BUTTON = - '[data-test-subj="create-saved-object-button"]'; -export const OVERVIEW_RISKY_HOSTS_DOC_LINK = - '[data-test-subj="risky-hosts-inner-panel-danger-learn-more"]'; -export const OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT = `${OVERVIEW_RISKY_HOSTS_LINKS} [data-test-subj="header-panel-subtitle"]`; -export const OVERVIEW_RISKY_HOSTS_ENABLE_MODULE_BUTTON = - '[data-test-subj="disabled-open-in-console-button-with-tooltip"]'; - export const OVERVIEW_ALERTS_HISTOGRAM = '[data-test-subj="alerts-histogram-panel"]'; diff --git a/x-pack/plugins/security_solution/cypress/support/index.js b/x-pack/plugins/security_solution/cypress/support/e2e.js similarity index 100% rename from x-pack/plugins/security_solution/cypress/support/index.js rename to x-pack/plugins/security_solution/cypress/support/e2e.js diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index a229693ed9f201..bd9c549eb0f40d 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -65,7 +65,8 @@ import { RULE_STATUS, RULE_TIMESTAMP_OVERRIDE, RULES_CREATION_FORM, - RULES_CREATION_PREVIEW, + RULES_CREATION_PREVIEW_BUTTON, + RULES_CREATION_PREVIEW_REFRESH_BUTTON, RUNS_EVERY_INTERVAL, RUNS_EVERY_TIME_TYPE, SCHEDULE_CONTINUE_BUTTON, @@ -336,15 +337,13 @@ export const fillDefineEqlRuleAndContinue = (rule: CustomRule) => { cy.get(RULES_CREATION_FORM).find(EQL_QUERY_INPUT).should('be.visible'); cy.get(RULES_CREATION_FORM).find(EQL_QUERY_INPUT).type(rule.customQuery); cy.get(RULES_CREATION_FORM).find(EQL_QUERY_VALIDATION_SPINNER).should('not.exist'); - cy.get(RULES_CREATION_PREVIEW) - .find(QUERY_PREVIEW_BUTTON) - .should('not.be.disabled') - .click({ force: true }); + cy.get(RULES_CREATION_PREVIEW_BUTTON).should('not.be.disabled').click({ force: true }); + cy.get(RULES_CREATION_PREVIEW_REFRESH_BUTTON).should('not.be.disabled').click({ force: true }); cy.get(PREVIEW_HISTOGRAM) .invoke('text') .then((text) => { if (text !== 'Rule Preview') { - cy.get(RULES_CREATION_PREVIEW).find(QUERY_PREVIEW_BUTTON).click({ force: true }); + cy.get(RULES_CREATION_PREVIEW_REFRESH_BUTTON).click({ force: true }); cy.get(PREVIEW_HISTOGRAM).should('contain.text', 'Rule Preview'); } }); diff --git a/x-pack/plugins/security_solution/cypress/upgrade_integration/detections/detection_rules/custom_query_rule.spec.ts b/x-pack/plugins/security_solution/cypress/upgrade_e2e/detections/detection_rules/custom_query_rule.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/upgrade_integration/detections/detection_rules/custom_query_rule.spec.ts rename to x-pack/plugins/security_solution/cypress/upgrade_e2e/detections/detection_rules/custom_query_rule.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/upgrade_integration/detections/detection_rules/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/upgrade_e2e/detections/detection_rules/threshold_rule.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/upgrade_integration/detections/detection_rules/threshold_rule.spec.ts rename to x-pack/plugins/security_solution/cypress/upgrade_e2e/detections/detection_rules/threshold_rule.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/upgrade_integration/threat_hunting/cases/import_case.spec.ts b/x-pack/plugins/security_solution/cypress/upgrade_e2e/threat_hunting/cases/import_case.cy.ts similarity index 98% rename from x-pack/plugins/security_solution/cypress/upgrade_integration/threat_hunting/cases/import_case.spec.ts rename to x-pack/plugins/security_solution/cypress/upgrade_e2e/threat_hunting/cases/import_case.cy.ts index 13afc4c7889eb6..4eecc36fe928aa 100644 --- a/x-pack/plugins/security_solution/cypress/upgrade_integration/threat_hunting/cases/import_case.spec.ts +++ b/x-pack/plugins/security_solution/cypress/upgrade_e2e/threat_hunting/cases/import_case.cy.ts @@ -13,7 +13,6 @@ import { ALL_CASES_NOT_PUSHED, ALL_CASES_NUMBER_OF_ALERTS, ALL_CASES_OPEN_CASES_STATS, - ALL_CASES_REPORTER, ALL_CASES_IN_PROGRESS_STATUS, } from '../../../screens/all_cases'; import { @@ -108,7 +107,6 @@ describe('Import case after upgrade', () => { it('Displays the correct case details on the cases page', () => { cy.get(ALL_CASES_NAME).should('have.text', importedCase.title); - cy.get(ALL_CASES_REPORTER).should('have.text', importedCase.initial); cy.get(ALL_CASES_NUMBER_OF_ALERTS).should('have.text', importedCase.numberOfAlerts); cy.get(ALL_CASES_COMMENTS_COUNT).should('have.text', importedCase.numberOfComments); cy.get(ALL_CASES_NOT_PUSHED).should('be.visible'); diff --git a/x-pack/plugins/security_solution/cypress/upgrade_integration/threat_hunting/timeline/import_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/upgrade_e2e/threat_hunting/timeline/import_timeline.cy.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/upgrade_integration/threat_hunting/timeline/import_timeline.spec.ts rename to x-pack/plugins/security_solution/cypress/upgrade_e2e/threat_hunting/timeline/import_timeline.cy.ts diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 92a934f2dd674d..987bcdbe271938 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -8,21 +8,20 @@ "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js && node ../../../scripts/eslint ./public/detections/mitre/mitre_tactics_techniques.ts --fix", "build-beat-doc": "node scripts/beat_docs/build.js && node ../../../scripts/eslint ../timelines/server/utils/beat_schema/fields.ts --fix", "cypress": "../../../node_modules/.bin/cypress", - "cypress:open": "yarn cypress open --config-file ./cypress/cypress.json", - "cypress:open:ccs": "yarn cypress:open --config integrationFolder=./cypress/ccs_integration", + "cypress:open": "yarn cypress open --config-file ./cypress/cypress.config.ts", + "cypress:open:ccs": "yarn cypress:open --config specPattern=./cypress/ccs_e2e/**/*.cy.ts", "cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/visual_config.ts", - "cypress:open:upgrade": "yarn cypress:open --config integrationFolder=./cypress/upgrade_integration", - "cypress:run": "yarn cypress:run:reporter --browser chrome --spec './cypress/integration/**/*.spec.ts'; status=$?; yarn junit:merge && exit $status", - "cypress:run:spec": "yarn cypress:run:reporter --browser chrome --spec ${SPEC_LIST:-'./cypress/integration/**/*.spec.ts'}; status=$?; yarn junit:merge && exit $status", - "cypress:run:cases": "yarn cypress:run:reporter --browser chrome --spec './cypress/integration/cases/*.spec.ts'; status=$?; yarn junit:merge && exit $status", - "cypress:run:firefox": "yarn cypress:run:reporter --browser firefox --spec './cypress/integration/**/*.spec.ts'; status=$?; yarn junit:merge && exit $status", - "cypress:run:reporter": "yarn cypress run --config-file ./cypress/cypress_ci.json --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json", - "cypress:run:respops": "yarn cypress:run:reporter --browser chrome --spec ./cypress/integration/detection_alerts/*.spec.ts,./cypress/integration/detection_rules/*.spec.ts,./cypress/integration/exceptions/*.spec.ts; status=$?; yarn junit:merge && exit $status", - "cypress:run:ccs": "yarn cypress:run:reporter --browser chrome --config integrationFolder=./cypress/ccs_integration; status=$?; yarn junit:merge && exit $status", + "cypress:open:upgrade": "yarn cypress:open --config specPattern=./cypress/upgrade_e2e/**/*.cy.ts", + "cypress:run": "yarn cypress:run:reporter --browser chrome --spec './cypress/e2e/**/*.cy.ts'; status=$?; yarn junit:merge && exit $status", + "cypress:run:spec": "yarn cypress:run:reporter --browser chrome --spec ${SPEC_LIST:-'./cypress/e2e/**/*.cy.ts'}; status=$?; yarn junit:merge && exit $status", + "cypress:run:cases": "yarn cypress:run:reporter --browser chrome --spec './cypress/e2e/cases/*.cy.ts'; status=$?; yarn junit:merge && exit $status", + "cypress:run:firefox": "yarn cypress:run:reporter --browser firefox --spec './cypress/e2e/**/*.cy.ts'; status=$?; yarn junit:merge && exit $status", + "cypress:run:reporter": "yarn cypress run --config-file ./cypress/cypress_ci.config.ts --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json", + "cypress:run:respops": "yarn cypress:run:reporter --browser chrome --spec ./cypress/e2e/detection_alerts/*.cy.ts,./cypress/e2e/detection_rules/*.cy.ts,./cypress/e2e/exceptions/*.cy.ts; status=$?; yarn junit:merge && exit $status", + "cypress:run:ccs": "yarn cypress:run:reporter --browser chrome --config specPattern=./cypress/ccs_e2e/**/*.cy.ts; status=$?; yarn junit:merge && exit $status", "cypress:run-as-ci": "node --max-old-space-size=2048 ../../../scripts/functional_tests --config ../../test/security_solution_cypress/cli_config_parallel.ts", "cypress:run-as-ci:firefox": "node --max-old-space-size=2048 ../../../scripts/functional_tests --config ../../test/security_solution_cypress/config.firefox.ts", - "cypress:run:upgrade": "yarn cypress:run:reporter --browser chrome --config integrationFolder=./cypress/upgrade_integration", - "cypress:run:upgrade:old": "yarn cypress:run:reporter --browser chrome --config integrationFolder=./cypress/upgrade_integration --spec ./cypress/upgrade_integration/threat_hunting/**/*.spec.ts,./cypress/upgrade_integration/detections/**/custom_query_rule.spec.ts; status=$?; yarn junit:merge && exit $status", + "cypress:run:upgrade": "yarn cypress:run:reporter --browser chrome --config specPattern=./cypress/upgrade_e2e/**/*.cy.ts", "junit:merge": "../../../node_modules/.bin/mochawesome-merge ../../../target/kibana-security-solution/cypress/results/mochawesome*.json > ../../../target/kibana-security-solution/cypress/results/output.json && ../../../node_modules/.bin/marge ../../../target/kibana-security-solution/cypress/results/output.json --reportDir ../../../target/kibana-security-solution/cypress/results && mkdir -p ../../../target/junit && cp ../../../target/kibana-security-solution/cypress/results/*.xml ../../../target/junit/", "test:generate": "node scripts/endpoint/resolver_generator" } diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index 7dad742861ac0c..6ac3a0aa7a3ff9 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -42,7 +42,7 @@ import { NETWORK, OVERVIEW, POLICIES, - RESPONSE_ACTIONS, + ACTION_HISTORY, ENTITY_ANALYTICS, RULES, TIMELINES, @@ -65,7 +65,7 @@ import { NETWORK_PATH, OVERVIEW_PATH, POLICIES_PATH, - RESPONSE_ACTIONS_PATH, + ACTION_HISTORY_PATH, ENTITY_ANALYTICS_PATH, RULES_CREATE_PATH, RULES_PATH, @@ -514,13 +514,13 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [ path: BLOCKLIST_PATH, }, { - ...getSecuritySolutionLink<SecurityPageName>('benchmarks'), - deepLinks: [getSecuritySolutionLink<SecurityPageName>('rules')], + id: SecurityPageName.actionHistory, + title: ACTION_HISTORY, + path: ACTION_HISTORY_PATH, }, { - id: SecurityPageName.responseActions, - title: RESPONSE_ACTIONS, - path: RESPONSE_ACTIONS_PATH, + ...getSecuritySolutionLink<SecurityPageName>('benchmarks'), + deepLinks: [getSecuritySolutionLink<SecurityPageName>('rules')], }, ], }, diff --git a/x-pack/plugins/security_solution/public/app/home/home_navigations.ts b/x-pack/plugins/security_solution/public/app/home/home_navigations.ts index 88ef1152b8f343..ae7c15c73a4d2f 100644 --- a/x-pack/plugins/security_solution/public/app/home/home_navigations.ts +++ b/x-pack/plugins/security_solution/public/app/home/home_navigations.ts @@ -30,7 +30,7 @@ import { APP_USERS_PATH, APP_KUBERNETES_PATH, APP_LANDING_PATH, - APP_RESPONSE_ACTIONS_PATH, + APP_ACTION_HISTORY_PATH, APP_ENTITY_ANALYTICS_PATH, APP_PATH, } from '../../../common/constants'; @@ -162,10 +162,10 @@ export const navTabs: SecurityNav = { disabled: false, urlKey: 'administration', }, - [SecurityPageName.responseActions]: { - id: SecurityPageName.responseActions, - name: i18n.RESPONSE_ACTIONS, - href: APP_RESPONSE_ACTIONS_PATH, + [SecurityPageName.actionHistory]: { + id: SecurityPageName.actionHistory, + name: i18n.ACTION_HISTORY, + href: APP_ACTION_HISTORY_PATH, disabled: false, urlKey: 'administration', }, diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index 400642bc1490d5..154127f469c961 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -120,12 +120,9 @@ export const BLOCKLIST = i18n.translate('xpack.securitySolution.navigation.block defaultMessage: 'Blocklist', }); -export const RESPONSE_ACTIONS = i18n.translate( - 'xpack.securitySolution.navigation.responseActions', - { - defaultMessage: 'Response Actions', - } -); +export const ACTION_HISTORY = i18n.translate('xpack.securitySolution.navigation.actionHistory', { + defaultMessage: 'Action history', +}); export const CREATE_NEW_RULE = i18n.translate('xpack.securitySolution.navigation.newRuleTitle', { defaultMessage: 'Create new rule', diff --git a/x-pack/plugins/security_solution/public/common/components/dashboards/dashboards_table.test.tsx b/x-pack/plugins/security_solution/public/common/components/dashboards/dashboards_table.test.tsx index 03f319b9cc97b8..55811743e4d45d 100644 --- a/x-pack/plugins/security_solution/public/common/components/dashboards/dashboards_table.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/dashboards/dashboards_table.test.tsx @@ -31,16 +31,15 @@ const DASHBOARD_TABLE_ITEMS = [ }, ]; -const mockUseSecurityDashboardsTableItems = jest.fn(() => DASHBOARD_TABLE_ITEMS); +const mockUseSecurityDashboardsTableItems = jest.fn(() => ({ + items: DASHBOARD_TABLE_ITEMS, + isLoading: false, +})); jest.mock('../../containers/dashboards/use_security_dashboards_table', () => { const actual = jest.requireActual('../../containers/dashboards/use_security_dashboards_table'); return { ...actual, - useSecurityDashboardsTable: () => { - const columns = actual.useSecurityDashboardsTableColumns(); - const items = mockUseSecurityDashboardsTableItems(); - return { columns, items }; - }, + useSecurityDashboardsTableItems: () => mockUseSecurityDashboardsTableItems(), }; }); diff --git a/x-pack/plugins/security_solution/public/common/components/dashboards/dashboards_table.tsx b/x-pack/plugins/security_solution/public/common/components/dashboards/dashboards_table.tsx index ee99a9966ab8aa..ba933becda53a9 100644 --- a/x-pack/plugins/security_solution/public/common/components/dashboards/dashboards_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/dashboards/dashboards_table.tsx @@ -9,7 +9,19 @@ import React, { useEffect, useMemo, useState } from 'react'; import { debounce } from 'lodash'; import type { Search } from '@elastic/eui'; import { EuiInMemoryTable } from '@elastic/eui'; -import { useSecurityDashboardsTable } from '../../containers/dashboards/use_security_dashboards_table'; +import { i18n } from '@kbn/i18n'; +import { + useSecurityDashboardsTableItems, + useSecurityDashboardsTableColumns, +} from '../../containers/dashboards/use_security_dashboards_table'; +import { useAppToasts } from '../../hooks/use_app_toasts'; + +export const DASHBOARDS_QUERY_ERROR = i18n.translate( + 'xpack.securitySolution.dashboards.queryError', + { + defaultMessage: 'Error retrieving security dashboards', + } +); /** wait this many ms after the user completes typing before applying the filter input */ const INPUT_TIMEOUT = 250; @@ -22,7 +34,10 @@ const DASHBOARDS_TABLE_SORTING = { } as const; export const DashboardsTable: React.FC = () => { - const { items, columns } = useSecurityDashboardsTable(); + const { items, isLoading, error } = useSecurityDashboardsTableItems(); + const columns = useSecurityDashboardsTableColumns(); + const { addError } = useAppToasts(); + const [filteredItems, setFilteredItems] = useState(items); const [searchQuery, setSearchQuery] = useState(''); @@ -52,6 +67,12 @@ export const DashboardsTable: React.FC = () => { } }, [items, searchQuery]); + useEffect(() => { + if (error) { + addError(error, { title: DASHBOARDS_QUERY_ERROR }); + } + }, [error, addError]); + return ( <EuiInMemoryTable data-test-subj="dashboardsTable" @@ -60,6 +81,7 @@ export const DashboardsTable: React.FC = () => { search={search} pagination={true} sorting={DASHBOARDS_TABLE_SORTING} + loading={isLoading} /> ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_form.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_form.tsx index ce1e1939ef1bbc..5bffe92e6d0199 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_form.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_form.tsx @@ -42,7 +42,7 @@ export const EndpointIsolateForm = memo<EndpointIsolatedFormProps>( ); return ( - <EuiForm> + <EuiForm data-test-subj="endpointHostIsolationForm"> <EuiFormRow fullWidth> <EuiText size="s"> <p> diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/unisolate_form.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/unisolate_form.tsx index 4e8358f3810fbd..8226175786ae07 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/unisolate_form.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/unisolate_form.tsx @@ -31,7 +31,7 @@ export const EndpointUnisolateForm = memo<EndpointIsolatedFormProps>( ); return ( - <EuiForm> + <EuiForm data-test-subj="endpointHostIsolationForm"> <EuiFormRow fullWidth> <EuiText size="s"> <p> @@ -62,7 +62,11 @@ export const EndpointUnisolateForm = memo<EndpointIsolatedFormProps>( <EuiFormRow fullWidth> <EuiFlexGroup justifyContent="flexEnd"> <EuiFlexItem grow={false}> - <EuiButtonEmpty onClick={onCancel} disabled={isLoading}> + <EuiButtonEmpty + onClick={onCancel} + disabled={isLoading} + data-test-subj="hostIsolateCancelButton" + > {CANCEL} </EuiButtonEmpty> </EuiFlexItem> diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.test.tsx index 945317036e7bcb..0c6cf454e73eec 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.test.tsx @@ -11,10 +11,11 @@ import { render } from '@testing-library/react'; import { TestProviders } from '../../../mock'; import { NO_HOST_RISK_DATA_DESCRIPTION } from './translations'; import { HostRiskSummary } from './host_risk_summary'; +import { RiskSeverity } from '../../../../../common/search_strategy'; describe('HostRiskSummary', () => { it('renders host risk data', () => { - const riskKeyword = 'test risk'; + const riskSeverity = RiskSeverity.low; const hostRisk = { loading: false, isModuleEnabled: true, @@ -23,11 +24,12 @@ describe('HostRiskSummary', () => { '@timestamp': '1641902481', host: { name: 'test-host-name', - }, - risk: riskKeyword, - risk_stats: { - risk_score: 9999, - rule_risks: [], + risk: { + multipliers: [], + calculated_score_norm: 9999, + calculated_level: riskSeverity, + rule_risks: [], + }, }, }, ], @@ -39,7 +41,7 @@ describe('HostRiskSummary', () => { </TestProviders> ); - expect(getByText(riskKeyword)).toBeInTheDocument(); + expect(getByText(riskSeverity)).toBeInTheDocument(); }); it('renders spinner when loading', () => { @@ -67,11 +69,12 @@ describe('HostRiskSummary', () => { '@timestamp': '1641902530', host: { name: 'test-host-name', - }, - risk: 'test-risk', - risk_stats: { - risk_score: 9999, - rule_risks: [], + risk: { + multipliers: [], + calculated_score_norm: 9999, + calculated_level: RiskSeverity.low, + rule_risks: [], + }, }, }, ], diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.tsx index 078fb0e1442cdb..970656933b938c 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.tsx @@ -9,11 +9,10 @@ import React from 'react'; import { EuiLoadingSpinner, EuiPanel, EuiSpacer, EuiLink, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import * as i18n from './translations'; -import { RISKY_HOSTS_DOC_LINK } from '../../../../overview/components/overview_risky_host_links/risky_hosts_disabled_module'; import { EnrichedDataRow, ThreatSummaryPanelHeader } from './threat_summary_view'; import { RiskScore } from '../../severity/common'; -import type { RiskSeverity } from '../../../../../common/search_strategy'; import type { HostRisk } from '../../../../risk_score/containers'; +import { RISKY_HOSTS_DOC_LINK } from '../../../../../common/constants'; const HostRiskSummaryComponent: React.FC<{ hostRisk: HostRisk; @@ -25,12 +24,12 @@ const HostRiskSummaryComponent: React.FC<{ toolTipContent={ <FormattedMessage id="xpack.securitySolution.alertDetails.overview.hostDataTooltipContent" - defaultMessage="Risk classification is displayed only when available for a host. Ensure {hostsRiskScoreDocumentationLink} is enabled within your environment." + defaultMessage="Risk classification is displayed only when available for a host. Ensure {hostRiskScoreDocumentationLink} is enabled within your environment." values={{ - hostsRiskScoreDocumentationLink: ( + hostRiskScoreDocumentationLink: ( <EuiLink href={RISKY_HOSTS_DOC_LINK} target="_blank"> <FormattedMessage - id="xpack.securitySolution.alertDetails.overview.hostsRiskScoreLink" + id="xpack.securitySolution.alertDetails.overview.hostRiskScoreLink" defaultMessage="Host Risk Score" /> </EuiLink> @@ -56,7 +55,10 @@ const HostRiskSummaryComponent: React.FC<{ <EnrichedDataRow field={i18n.HOST_RISK_CLASSIFICATION} value={ - <RiskScore severity={hostRisk.result[0].risk as RiskSeverity} hideBackgroundColor /> + <RiskScore + severity={hostRisk.result[0].host.risk.calculated_level} + hideBackgroundColor + /> } /> </> diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx index 5148dde4d6b596..4e9ff49a2b1dd2 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx @@ -7,10 +7,11 @@ import { EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; import { ALERT_RULE_UUID } from '@kbn/rule-data-utils'; - -import React, { useMemo } from 'react'; +import React, { createContext, useMemo } from 'react'; import styled from 'styled-components'; +import type { GetBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; import * as i18n from './translations'; import { useRuleWithFallback } from '../../../detections/containers/detection_engine/rules/use_rule_with_fallback'; import { MarkdownRenderer } from '../markdown_editor'; @@ -22,6 +23,8 @@ export const Indent = styled.div` word-break: break-word; `; +export const BasicAlertDataContext = createContext<Partial<GetBasicDataFromDetailsData>>({}); + const InvestigationGuideViewComponent: React.FC<{ data: TimelineEventsDetailsItem[]; }> = ({ data }) => { @@ -32,13 +35,14 @@ const InvestigationGuideViewComponent: React.FC<{ : item?.originalValue ?? null; }, [data]); const { rule: maybeRule } = useRuleWithFallback(ruleId); + const basicAlertData = useBasicDataFromDetailsData(data); if (!maybeRule?.note) { return null; } return ( - <> + <BasicAlertDataContext.Provider value={basicAlertData}> <EuiSpacer size="l" /> <EuiTitle size="xxxs" data-test-subj="summary-view-guide"> <h5>{i18n.INVESTIGATION_GUIDE}</h5> @@ -51,7 +55,7 @@ const InvestigationGuideViewComponent: React.FC<{ </LineClamp> </EuiText> </Indent> - </> + </BasicAlertDataContext.Provider> ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts index c7f8481c362470..494ecb0c6b4d0e 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts @@ -5,37 +5,27 @@ * 2.0. */ -import type { EuiLinkAnchorProps } from '@elastic/eui'; import { getDefaultEuiMarkdownParsingPlugins, getDefaultEuiMarkdownProcessingPlugins, getDefaultEuiMarkdownUiPlugins, } from '@elastic/eui'; -// Remove after this issue is resolved: https://github.com/elastic/eui/issues/4688 -import type { Options as Remark2RehypeOptions } from 'mdast-util-to-hast'; -import type { FunctionComponent } from 'react'; -import type rehype2react from 'rehype-react'; -import type { Plugin, PluggableList } from 'unified'; + import * as timelineMarkdownPlugin from './timeline'; +import * as osqueryMarkdownPlugin from './osquery'; export const { uiPlugins, parsingPlugins, processingPlugins } = { uiPlugins: getDefaultEuiMarkdownUiPlugins(), parsingPlugins: getDefaultEuiMarkdownParsingPlugins(), - processingPlugins: getDefaultEuiMarkdownProcessingPlugins() as [ - [Plugin, Remark2RehypeOptions], - [ - typeof rehype2react, - Parameters<typeof rehype2react>[0] & { - components: { a: FunctionComponent<EuiLinkAnchorProps>; timeline: unknown }; - } - ], - ...PluggableList - ], + processingPlugins: getDefaultEuiMarkdownProcessingPlugins(), }; uiPlugins.push(timelineMarkdownPlugin.plugin); +uiPlugins.push(osqueryMarkdownPlugin.plugin); parsingPlugins.push(timelineMarkdownPlugin.parser); +parsingPlugins.push(osqueryMarkdownPlugin.parser); // This line of code is TS-compatible and it will break if [1][1] change in the future. processingPlugins[1][1].components.timeline = timelineMarkdownPlugin.renderer; +processingPlugins[1][1].components.osquery = osqueryMarkdownPlugin.renderer; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/index.tsx new file mode 100644 index 00000000000000..7b96f3886159c5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/index.tsx @@ -0,0 +1,273 @@ +/* + * 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 { pickBy, isEmpty } from 'lodash'; +import type { Plugin } from 'unified'; +import React, { useContext, useMemo, useState, useCallback } from 'react'; +import type { RemarkTokenizer } from '@elastic/eui'; +import { + EuiSpacer, + EuiCodeBlock, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiButton, + EuiButtonEmpty, +} from '@elastic/eui'; +import { useForm, FormProvider } from 'react-hook-form'; +import styled from 'styled-components'; +import type { EuiMarkdownEditorUiPluginEditorProps } from '@elastic/eui/src/components/markdown_editor/markdown_types'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '../../../../lib/kibana'; +import { LabelField } from './label_field'; +import OsqueryLogo from './osquery_icon/osquery.svg'; +import { OsqueryFlyout } from '../../../../../detections/components/osquery/osquery_flyout'; +import { BasicAlertDataContext } from '../../../event_details/investigation_guide_view'; +import { convertECSMappingToObject } from './utils'; + +const StyledEuiButton = styled(EuiButton)` + > span > img { + margin-block-end: 0; + } +`; + +const OsqueryEditorComponent = ({ + node, + onSave, + onCancel, +}: EuiMarkdownEditorUiPluginEditorProps<{ + configuration: { + label?: string; + query: string; + ecs_mapping: { [key: string]: {} }; + }; +}>) => { + const isEditMode = node != null; + const { osquery } = useKibana().services; + const formMethods = useForm<{ + label: string; + query: string; + ecs_mapping: Record<string, unknown>; + }>({ + defaultValues: { + label: node?.configuration?.label, + query: node?.configuration?.query, + ecs_mapping: node?.configuration?.ecs_mapping, + }, + }); + + const onSubmit = useCallback( + (data) => { + onSave( + `!{osquery${JSON.stringify( + pickBy( + { + query: data.query, + label: data.label, + ecs_mapping: convertECSMappingToObject(data.ecs_mapping), + }, + (value) => !isEmpty(value) + ) + )}}`, + { + block: true, + } + ); + }, + [onSave] + ); + + const OsqueryActionForm = useMemo(() => { + if (osquery?.LiveQueryField) { + const { LiveQueryField } = osquery; + + return ( + <FormProvider {...formMethods}> + <LabelField /> + <EuiSpacer size="m" /> + <LiveQueryField formMethods={formMethods} /> + </FormProvider> + ); + } + return null; + }, [formMethods, osquery]); + + return ( + <> + <EuiModalHeader> + <EuiModalHeaderTitle> + {isEditMode ? ( + <FormattedMessage + id="xpack.securitySolution.markdown.osquery.editModalTitle" + defaultMessage="Edit query" + /> + ) : ( + <FormattedMessage + id="xpack.securitySolution.markdown.osquery.addModalTitle" + defaultMessage="Add query" + /> + )} + </EuiModalHeaderTitle> + </EuiModalHeader> + + <EuiModalBody> + <>{OsqueryActionForm}</> + </EuiModalBody> + + <EuiModalFooter> + <EuiButtonEmpty onClick={onCancel}> + {i18n.translate('xpack.securitySolution.markdown.osquery.modalCancelButtonLabel', { + defaultMessage: 'Cancel', + })} + </EuiButtonEmpty> + <EuiButton onClick={formMethods.handleSubmit(onSubmit)} fill> + {isEditMode ? ( + <FormattedMessage + id="xpack.securitySolution.markdown.osquery.addModalConfirmButtonLabel" + defaultMessage="Add query" + /> + ) : ( + <FormattedMessage + id="xpack.securitySolution.markdown.osquery.editModalConfirmButtonLabel" + defaultMessage="Save changes" + /> + )} + </EuiButton> + </EuiModalFooter> + </> + ); +}; + +const OsqueryEditor = React.memo(OsqueryEditorComponent); + +export const plugin = { + name: 'osquery', + button: { + label: 'Osquery', + iconType: 'logoOsquery', + }, + helpText: ( + <div> + <EuiCodeBlock language="md" fontSize="l" paddingSize="s" isCopyable> + {'!{osquery{options}}'} + </EuiCodeBlock> + <EuiSpacer size="s" /> + </div> + ), + editor: OsqueryEditor, +}; + +export const parser: Plugin = function () { + const Parser = this.Parser; + const tokenizers = Parser.prototype.blockTokenizers; + const methods = Parser.prototype.blockMethods; + + const tokenizeOsquery: RemarkTokenizer = function (eat, value, silent) { + if (value.startsWith('!{osquery') === false) return false; + + const nextChar = value[9]; + + if (nextChar !== '{' && nextChar !== '}') return false; // this isn't actually a osquery + + if (silent) { + return true; + } + + // is there a configuration? + const hasConfiguration = nextChar === '{'; + + let match = '!{osquery'; + let configuration = {}; + + if (hasConfiguration) { + let configurationString = ''; + + let openObjects = 0; + + for (let i = 9; i < value.length; i++) { + const char = value[i]; + if (char === '{') { + openObjects++; + configurationString += char; + } else if (char === '}') { + openObjects--; + if (openObjects === -1) { + break; + } + configurationString += char; + } else { + configurationString += char; + } + } + + match += configurationString; + try { + configuration = JSON.parse(configurationString); + } catch (e) { + const now = eat.now(); + this.file.fail(`Unable to parse osquery JSON configuration: ${e}`, { + line: now.line, + column: now.column + 9, + }); + } + } + + match += '}'; + + return eat(match)({ + type: 'osquery', + configuration, + }); + }; + + tokenizers.osquery = tokenizeOsquery; + methods.splice(methods.indexOf('text'), 0, 'osquery'); +}; + +// receives the configuration from the parser and renders +const RunOsqueryButtonRenderer = ({ + configuration, +}: { + configuration: { + label?: string; + query: string; + ecs_mapping: { [key: string]: {} }; + }; +}) => { + const [showFlyout, setShowFlyout] = useState(false); + const { agentId } = useContext(BasicAlertDataContext); + + const handleOpen = useCallback(() => setShowFlyout(true), [setShowFlyout]); + + const handleClose = useCallback(() => setShowFlyout(false), [setShowFlyout]); + + return ( + <> + <StyledEuiButton iconType={OsqueryLogo} onClick={handleOpen}> + {configuration.label ?? + i18n.translate('xpack.securitySolution.markdown.osquery.runOsqueryButtonLabel', { + defaultMessage: 'Run Osquery', + })} + </StyledEuiButton> + {showFlyout && ( + <OsqueryFlyout + defaultValues={{ + query: configuration.query, + ecs_mapping: configuration.ecs_mapping, + queryField: false, + }} + agentId={agentId} + onClose={handleClose} + /> + )} + </> + ); +}; + +export { RunOsqueryButtonRenderer as renderer }; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/label_field.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/label_field.tsx new file mode 100644 index 00000000000000..3517bbf7643d30 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/label_field.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useMemo } from 'react'; +import { useController } from 'react-hook-form'; +import { EuiFieldText, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface QueryDescriptionFieldProps { + euiFieldProps?: Record<string, unknown>; +} + +const LabelFieldComponent = ({ euiFieldProps }: QueryDescriptionFieldProps) => { + const { + field: { onChange, value, name: fieldName }, + fieldState: { error }, + } = useController({ + name: 'label', + defaultValue: '', + }); + + const hasError = useMemo(() => !!error?.message, [error?.message]); + + return ( + <EuiFormRow + label={i18n.translate('xpack.securitySolution.markdown.osquery.labelFieldText', { + defaultMessage: 'Label', + })} + error={error?.message} + isInvalid={hasError} + fullWidth + > + <EuiFieldText + isInvalid={hasError} + onChange={onChange} + value={value} + name={fieldName} + fullWidth + data-test-subj="input" + {...euiFieldProps} + /> + </EuiFormRow> + ); +}; + +export const LabelField = React.memo(LabelFieldComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/osquery_icon/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/osquery_icon/index.tsx new file mode 100644 index 00000000000000..fe7b811bd70fdc --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/osquery_icon/index.tsx @@ -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 React from 'react'; +import type { EuiIconProps } from '@elastic/eui'; +import { EuiIcon } from '@elastic/eui'; +import OsqueryLogo from './osquery.svg'; + +export type OsqueryIconProps = Omit<EuiIconProps, 'type'>; + +const OsqueryIconComponent: React.FC<OsqueryIconProps> = (props) => ( + <EuiIcon size="xl" type={OsqueryLogo} {...props} /> +); + +export const OsqueryIcon = React.memo(OsqueryIconComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/osquery_icon/osquery.svg b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/osquery_icon/osquery.svg new file mode 100755 index 00000000000000..32305a5916c042 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/osquery_icon/osquery.svg @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="256px" height="255px" viewBox="0 0 256 255" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"> + <g> + <path d="M255.214617,0.257580247 L255.214617,63.993679 L191.609679,127.598617 L191.609679,63.7297778 L255.214617,0.257580247" fill="#A596FF"></path> + <path d="M128.006321,0.257580247 L128.006321,63.993679 L191.611259,127.598617 L191.611259,63.7297778 L128.006321,0.257580247" fill="#000000"></path> + <path d="M255.345778,254.803753 L191.609679,254.803753 L128.004741,191.198815 L191.872,191.198815 L255.345778,254.803753" fill="#A596FF"></path> + <path d="M255.345778,127.595457 L191.609679,127.595457 L128.004741,191.200395 L191.872,191.200395 L255.345778,127.595457" fill="#000000"></path> + <path d="M0.801185185,254.936494 L0.801185185,191.198815 L64.4061235,127.593877 L64.4061235,191.462716 L0.801185185,254.936494" fill="#A596FF"></path> + <path d="M128.009481,254.936494 L128.009481,191.198815 L64.4045432,127.593877 L64.4045432,191.462716 L128.009481,254.936494" fill="#000000"></path> + <path d="M0.671604938,0.385580247 L64.4077037,0.385580247 L128.012642,63.9905185 L64.1453827,63.9905185 L0.671604938,0.385580247" fill="#A596FF"></path> + <path d="M0.671604938,127.593877 L64.4077037,127.593877 L128.012642,63.9889383 L64.1453827,63.9889383 L0.671604938,127.593877" fill="#000000"></path> + </g> +</svg> diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/utils.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/utils.ts new file mode 100644 index 00000000000000..77e2f14c514203 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/utils.ts @@ -0,0 +1,31 @@ +/* + * 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 { isEmpty, reduce } from 'lodash'; + +export const convertECSMappingToObject = ( + ecsMapping: Array<{ + key: string; + result: { + type: string; + value: string; + }; + }> +) => + reduce( + ecsMapping, + (acc, value) => { + if (!isEmpty(value?.key) && !isEmpty(value.result?.type) && !isEmpty(value.result?.value)) { + acc[value.key] = { + [value.result.type]: value.result.value, + }; + } + + return acc; + }, + {} as Record<string, { field?: string; value?: string }> + ); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts index 06732f83f2af7a..5fa33c8becfcab 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useState, useEffect, useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { DEFAULT_ANOMALY_SCORE } from '../../../../../common/constants'; import { anomaliesTableData } from '../api/anomalies_table_data'; @@ -14,6 +14,7 @@ import type { InfluencerInput, Anomalies, CriteriaFields } from '../types'; import * as i18n from './translations'; import { useTimeZone, useUiSetting$ } from '../../../lib/kibana'; import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useFetch, REQUEST_NAMES } from '../../../hooks/use_fetch'; import { useMlCapabilities } from '../hooks/use_ml_capabilities'; import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; @@ -63,11 +64,9 @@ export const useAnomaliesTableData = ({ jobIds, aggregationInterval, }: Args): Return => { - const [tableData, setTableData] = useState<Anomalies | null>(null); const mlCapabilities = useMlCapabilities(); const isMlUser = hasMlUserPermissions(mlCapabilities); - const [loading, setLoading] = useState(true); const { addError } = useAppToasts(); const timeZone = useTimeZone(); const [anomalyScore] = useUiSetting$<number>(DEFAULT_ANOMALY_SCORE); @@ -75,62 +74,35 @@ export const useAnomaliesTableData = ({ const startDateMs = useMemo(() => new Date(startDate).getTime(), [startDate]); const endDateMs = useMemo(() => new Date(endDate).getTime(), [endDate]); - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - setLoading(true); + const { + fetch, + data = null, + isLoading, + error, + } = useFetch(REQUEST_NAMES.ANOMALIES_TABLE, anomaliesTableData, { disabled: skip }); - async function fetchAnomaliesTableData( - influencersInput: InfluencerInput[], - criteriaFieldsInput: CriteriaFields[], - earliestMs: number, - latestMs: number - ) { - if (skip) { - setLoading(false); - } else if (isMlUser && !skip && jobIds.length > 0) { - try { - const data = await anomaliesTableData( - { - jobIds, - criteriaFields: criteriaFieldsInput, - influencersFilterQuery: filterQuery, - aggregationInterval, - threshold: getThreshold(anomalyScore, threshold), - earliestMs, - latestMs, - influencers: influencersInput, - dateFormatTz: timeZone, - maxRecords: 500, - maxExamples: 10, - }, - abortCtrl.signal - ); - if (isSubscribed) { - setTableData(data); - setLoading(false); - } - } catch (error) { - if (isSubscribed) { - addError(error, { title: i18n.SIEM_TABLE_FETCH_FAILURE }); - setLoading(false); - } - } - } else if (!isMlUser && isSubscribed) { - setLoading(false); - } else if (jobIds.length === 0 && isSubscribed) { - setLoading(false); - } else if (isSubscribed) { - setTableData(null); - setLoading(true); - } + useEffect(() => { + if (error) { + addError(error, { title: i18n.SIEM_TABLE_FETCH_FAILURE }); } + }, [error, addError]); - fetchAnomaliesTableData(influencers, criteriaFields, startDateMs, endDateMs); - return () => { - isSubscribed = false; - abortCtrl.abort(); - }; + useEffect(() => { + if (isMlUser && jobIds.length > 0) { + fetch({ + jobIds, + criteriaFields, + influencersFilterQuery: filterQuery, + aggregationInterval, + threshold: getThreshold(anomalyScore, threshold), + earliestMs: startDateMs, + latestMs: endDateMs, + influencers, + dateFormatTz: timeZone, + maxRecords: 500, + maxExamples: 10, + }); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ // eslint-disable-next-line react-hooks/exhaustive-deps @@ -139,12 +111,11 @@ export const useAnomaliesTableData = ({ influencersOrCriteriaToString(criteriaFields), startDateMs, endDateMs, - skip, isMlUser, aggregationInterval, // eslint-disable-next-line react-hooks/exhaustive-deps jobIds.sort().join(), ]); - return [loading, tableData]; + return [isLoading, data]; }; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts index 99ce1198f30d76..5a4c346be2e12d 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts @@ -65,6 +65,7 @@ export interface NavTab { } export const securityNavKeys = [ SecurityPageName.alerts, + SecurityPageName.actionHistory, SecurityPageName.blocklist, SecurityPageName.detectionAndResponse, SecurityPageName.case, @@ -77,7 +78,6 @@ export const securityNavKeys = [ SecurityPageName.hosts, SecurityPageName.network, SecurityPageName.overview, - SecurityPageName.responseActions, SecurityPageName.rules, SecurityPageName.timelines, SecurityPageName.trustedApps, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap index 3b87593d9f4831..a7f54ccf701b81 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap @@ -240,6 +240,16 @@ Object { "name": "Blocklist", "onClick": [Function], }, + Object { + "data-href": "securitySolutionUI/action_history", + "data-test-subj": "navigation-action_history", + "disabled": false, + "href": "securitySolutionUI/action_history", + "id": "action_history", + "isSelected": false, + "name": "Action history", + "onClick": [Function], + }, Object { "data-href": "securitySolutionUI/cloud_security_posture-benchmarks", "data-test-subj": "navigation-cloud_security_posture-benchmarks", diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx index ea1448e57398bc..2a8d977760cbfc 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx @@ -147,6 +147,7 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record<string, NavTab>) { ? [navTabs[SecurityPageName.hostIsolationExceptions]] : []), navTabs[SecurityPageName.blocklist], + navTabs[SecurityPageName.actionHistory], navTabs[SecurityPageName.cloudSecurityPostureBenchmarks], ], }, diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx index fadf1767b1db6f..b9e95a2ee837e3 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx @@ -15,8 +15,6 @@ import { FilterManager } from '@kbn/data-plugin/public'; import { SearchBar } from '@kbn/unified-search-plugin/public'; import type { QueryBarComponentProps } from '.'; import { QueryBar } from '.'; -import { setAutocomplete } from '@kbn/unified-search-plugin/public/services'; -import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; @@ -275,11 +273,6 @@ describe('QueryBar ', () => { }); describe('#onSavedQueryUpdated', () => { - beforeEach(() => { - const autocompleteStart = unifiedSearchPluginMock.createStartContract(); - setAutocomplete(autocompleteStart.autocomplete); - }); - test('is only reference that changed when dataProviders props get updated', async () => { await act(async () => { const wrapper = await getWrapper( diff --git a/x-pack/plugins/security_solution/public/common/components/risk_score_over_time/index.tsx b/x-pack/plugins/security_solution/public/common/components/risk_score_over_time/index.tsx index 5c6979fbd4a039..d17621ade7956f 100644 --- a/x-pack/plugins/security_solution/public/common/components/risk_score_over_time/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/risk_score_over_time/index.tsx @@ -27,13 +27,14 @@ import { HeaderSection } from '../header_section'; import { InspectButton, InspectButtonContainer } from '../inspect'; import * as i18n from './translations'; import { PreferenceFormattedDate } from '../formatted_date'; -import type { RiskScore } from '../../../../common/search_strategy'; +import type { HostRiskScore, UserRiskScore } from '../../../../common/search_strategy'; +import { isUserRiskScore } from '../../../../common/search_strategy'; export interface RiskScoreOverTimeProps { from: string; to: string; loading: boolean; - riskScore?: RiskScore[]; + riskScore?: Array<HostRiskScore | UserRiskScore>; queryId: string; title: string; toggleStatus: boolean; @@ -81,7 +82,7 @@ const RiskScoreOverTimeComponent: React.FC<RiskScoreOverTimeProps> = ({ riskScore ?.map((data) => ({ x: data['@timestamp'], - y: data.risk_stats.risk_score, + y: (isUserRiskScore(data) ? data.user : data.host).risk.calculated_score_norm, })) .reverse() ?? [], [riskScore] diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.test.tsx b/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.test.tsx index 43da2bd760b3b3..bc02fb3b205d68 100644 --- a/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.test.tsx @@ -99,9 +99,9 @@ describe('Security Dashboards Table hooks', () => { it('should return a memoized value when rerendered', async () => { const { result, rerender } = await renderUseSecurityDashboardsTableItems(); - const result1 = result.current; + const result1 = result.current.items; act(() => rerender()); - const result2 = result.current; + const result2 = result.current.items; expect(result1).toBe(result2); }); @@ -110,7 +110,7 @@ describe('Security Dashboards Table hooks', () => { const { result } = await renderUseSecurityDashboardsTableItems(); const [dashboard1, dashboard2] = DASHBOARDS_RESPONSE; - expect(result.current).toStrictEqual([ + expect(result.current.items).toStrictEqual([ { ...dashboard1, title: dashboard1.attributes.title, @@ -148,7 +148,7 @@ describe('Security Dashboards Table hooks', () => { }); it('returns a memoized value', async () => { - const { result, rerender } = await renderUseSecurityDashboardsTableItems(); + const { result, rerender } = renderUseDashboardsTableColumns(); const result1 = result.current; act(() => rerender()); @@ -163,7 +163,7 @@ describe('Security Dashboards Table hooks', () => { const { result: columnsResult } = renderUseDashboardsTableColumns(); const result = render( - <EuiBasicTable items={itemsResult.current} columns={columnsResult.current} />, + <EuiBasicTable items={itemsResult.current.items} columns={columnsResult.current} />, { wrapper: TestProviders, } diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.tsx b/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.tsx index 0fe9bbede41530..71fad3639dc5c6 100644 --- a/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, useEffect, useMemo, useCallback } from 'react'; +import React, { useEffect, useMemo, useCallback } from 'react'; import type { MouseEventHandler } from 'react'; import type { EuiBasicTableColumn } from '@elastic/eui'; import type { SavedObjectAttributes } from '@kbn/securitysolution-io-ts-alerting-types'; @@ -14,6 +14,7 @@ import { getSecurityDashboards } from './utils'; import { LinkAnchor } from '../../components/links'; import { useKibana, useNavigateTo } from '../../lib/kibana'; import * as i18n from './translations'; +import { useFetch, REQUEST_NAMES } from '../../hooks/use_fetch'; export interface DashboardTableItem extends SavedObject<SavedObjectAttributes> { title?: string; @@ -23,37 +24,33 @@ export interface DashboardTableItem extends SavedObject<SavedObjectAttributes> { const EMPTY_DESCRIPTION = '-' as const; export const useSecurityDashboardsTableItems = () => { - const [dashboardItems, setDashboardItems] = useState<DashboardTableItem[]>([]); const { savedObjects: { client: savedObjectsClient }, } = useKibana().services; - useEffect(() => { - let ignore = false; - const fetchDashboards = async () => { - if (savedObjectsClient) { - const securityDashboards = await getSecurityDashboards(savedObjectsClient); - - if (!ignore) { - setDashboardItems( - securityDashboards.map((securityDashboard) => ({ - ...securityDashboard, - title: securityDashboard.attributes.title?.toString() ?? undefined, - description: securityDashboard.attributes.description?.toString() ?? undefined, - })) - ); - } - } - }; + const { fetch, data, isLoading, error } = useFetch( + REQUEST_NAMES.SECURITY_DASHBOARDS, + getSecurityDashboards + ); - fetchDashboards(); + useEffect(() => { + if (savedObjectsClient) { + fetch(savedObjectsClient); + } + }, [fetch, savedObjectsClient]); - return () => { - ignore = true; - }; - }, [savedObjectsClient]); + const items = useMemo(() => { + if (!data) { + return []; + } + return data.map((securityDashboard) => ({ + ...securityDashboard, + title: securityDashboard.attributes.title?.toString() ?? undefined, + description: securityDashboard.attributes.description?.toString() ?? undefined, + })); + }, [data]); - return dashboardItems; + return { items, isLoading, error }; }; export const useSecurityDashboardsTableColumns = (): Array< @@ -104,9 +101,3 @@ export const useSecurityDashboardsTableColumns = (): Array< return columns; }; - -export const useSecurityDashboardsTable = () => { - const items = useSecurityDashboardsTableItems(); - const columns = useSecurityDashboardsTableColumns(); - return { items, columns }; -}; diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx index 40a473de306871..41383d4a0eb722 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx @@ -24,6 +24,7 @@ jest.mock('react-redux', () => { }; }); jest.mock('../../lib/kibana'); +jest.mock('../../lib/apm/use_track_http_request'); describe('source/index.tsx', () => { describe('getAllBrowserFields', () => { diff --git a/x-pack/plugins/security_solution/public/common/containers/source/use_data_view.tsx b/x-pack/plugins/security_solution/public/common/containers/source/use_data_view.tsx index 51ad895b56f0ca..c259f8843263af 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/use_data_view.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/use_data_view.tsx @@ -25,6 +25,8 @@ import { sourcererActions } from '../../store/sourcerer'; import * as i18n from './translations'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { getSourcererDataView } from '../sourcerer/api'; +import { useTrackHttpRequest } from '../../lib/apm/use_track_http_request'; +import { APP_UI_ID } from '../../../../common/constants'; export type IndexFieldSearch = (param: { dataViewId: string; @@ -86,6 +88,7 @@ export const useDataView = (): { const searchSubscription$ = useRef<Record<string, Subscription>>({}); const dispatch = useDispatch(); const { addError, addWarning } = useAppToasts(); + const { startTracking } = useTrackHttpRequest(); const setLoading = useCallback( ({ id, loading }: { id: string; loading: boolean }) => { @@ -112,6 +115,9 @@ export const useDataView = (): { [dataViewId]: new AbortController(), }; setLoading({ id: dataViewId, loading: true }); + + const { endTracking } = startTracking({ name: `${APP_UI_ID} indexFieldsSearch` }); + if (needToBeInit) { const dataViewToUpdate = await getSourcererDataView( dataViewId, @@ -139,6 +145,8 @@ export const useDataView = (): { .subscribe({ next: async (response) => { if (isCompleteResponse(response)) { + endTracking('success'); + const patternString = response.indicesExist.sort().join(); if (needToBeInit && scopeId) { dispatch( @@ -167,6 +175,7 @@ export const useDataView = (): { }) ); } else if (isErrorResponse(response)) { + endTracking('invalid'); setLoading({ id: dataViewId, loading: false }); addWarning(i18n.ERROR_BEAT_FIELDS); } @@ -174,6 +183,7 @@ export const useDataView = (): { resolve(); }, error: (msg) => { + endTracking('error'); if (msg.message === DELETED_SECURITY_SOLUTION_DATA_VIEW) { // reload app if security solution data view is deleted return location.reload(); @@ -200,7 +210,7 @@ export const useDataView = (): { } return asyncSearch(); }, - [addError, addWarning, data.search, dispatch, setLoading] + [addError, addWarning, data.search, dispatch, setLoading, startTracking] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx index b99547db88ba40..9be9c1266c1c95 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx @@ -46,6 +46,7 @@ const mockRouteSpy: RouteSpyState = { }; const mockDispatch = jest.fn(); const mockUseUserInfo = useUserInfo as jest.Mock; +jest.mock('../../lib/apm/use_track_http_request'); jest.mock('../../../detections/components/user_info'); jest.mock('./api'); jest.mock('../../utils/global_query_string'); diff --git a/x-pack/plugins/security_solution/public/common/hooks/search_bar/use_init_timerange_url_params.ts b/x-pack/plugins/security_solution/public/common/hooks/search_bar/use_init_timerange_url_params.ts index 673ca827c53021..824733a4f18fdb 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/search_bar/use_init_timerange_url_params.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/search_bar/use_init_timerange_url_params.ts @@ -60,20 +60,14 @@ const initializeTimerangeFromUrlParam = ( if (isEmpty(globalLinkTo.linkTo)) { dispatch(inputsActions.removeLinkTo([InputsModelId.global, InputsModelId.timeline])); - dispatch( - inputsActions.removeLinkTo([ - InputsModelId.global, - ...(isSocTrendsEnabled ? [InputsModelId.socTrends] : []), - ]) - ); + if (isSocTrendsEnabled) { + dispatch(inputsActions.removeLinkTo([InputsModelId.global, InputsModelId.socTrends])); + } } else { dispatch(inputsActions.addLinkTo([InputsModelId.global, InputsModelId.timeline])); - dispatch( - inputsActions.addLinkTo([ - InputsModelId.global, - ...(isSocTrendsEnabled ? [InputsModelId.socTrends] : []), - ]) - ); + if (isSocTrendsEnabled) { + dispatch(inputsActions.addLinkTo([InputsModelId.global, InputsModelId.socTrends])); + } } if (isEmpty(timelineLinkTo.linkTo)) { diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_fetch/index.ts b/x-pack/plugins/security_solution/public/common/hooks/use_fetch/index.ts new file mode 100644 index 00000000000000..fae594e8414ce4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_fetch/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { useFetch } from './use_fetch'; +export { REQUEST_NAMES } from './request_names'; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_fetch/request_names.ts b/x-pack/plugins/security_solution/public/common/hooks/use_fetch/request_names.ts new file mode 100644 index 00000000000000..cadfd2a68fa326 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_fetch/request_names.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 { APP_UI_ID } from '../../../../common/constants'; + +export const REQUEST_NAMES = { + SECURITY_DASHBOARDS: `${APP_UI_ID} fetch security dashboards`, + ANOMALIES_TABLE: `${APP_UI_ID} fetch anomalies table data`, +} as const; + +export type RequestName = typeof REQUEST_NAMES[keyof typeof REQUEST_NAMES]; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_fetch/use_fetch.test.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_fetch/use_fetch.test.tsx new file mode 100644 index 00000000000000..13d758ed7a596e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_fetch/use_fetch.test.tsx @@ -0,0 +1,295 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import type { RequestName } from './request_names'; +import type { OptionsParam, RequestFnParam, Result } from './use_fetch'; +import { useFetch } from './use_fetch'; + +export const mockEndTracking = jest.fn(); +export const mockStartTracking = jest.fn(() => ({ endTracking: mockEndTracking })); +jest.mock('../../lib/apm/use_track_http_request', () => ({ + useTrackHttpRequest: jest.fn(() => ({ + startTracking: mockStartTracking, + })), +})); + +const requestName = 'test name' as RequestName; + +const parameters = { + some: 'fakeParam', +}; +type Parameters = typeof parameters; + +const response = 'someData'; +const mockFetchFn = jest.fn(async (_: Parameters) => response); + +type UseFetchParams = [RequestName, RequestFnParam<Parameters, string>, OptionsParam<Parameters>]; + +const abortController = new AbortController(); + +const renderUseFetch = (options?: OptionsParam<Parameters>) => + renderHook<UseFetchParams, Result<Parameters, string, unknown>>(() => + useFetch(requestName, mockFetchFn, options) + ); + +describe('useFetch', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('init', async () => { + const { result } = renderUseFetch(); + + const { data, isLoading, error } = result.current; + + expect(data).toEqual(undefined); + expect(isLoading).toEqual(false); + expect(error).toEqual(undefined); + + expect(mockFetchFn).not.toHaveBeenCalled(); + }); + + it('should call fetch', async () => { + const { result, waitForNextUpdate } = renderUseFetch(); + + expect(result.current.data).toEqual(undefined); + expect(result.current.isLoading).toEqual(false); + expect(result.current.error).toEqual(undefined); + + await act(async () => { + result.current.fetch(parameters); + await waitForNextUpdate(); + }); + + expect(result.current.data).toEqual(response); + expect(result.current.isLoading).toEqual(false); + expect(result.current.error).toEqual(undefined); + expect(mockFetchFn).toHaveBeenCalledWith(parameters, abortController.signal); + }); + + it('should call fetch if initialParameters option defined', async () => { + const { result, waitForNextUpdate } = renderUseFetch({ initialParameters: parameters }); + + expect(result.current.data).toEqual(undefined); + expect(result.current.isLoading).toEqual(true); + expect(result.current.error).toEqual(undefined); + + expect(mockFetchFn).toHaveBeenCalledWith(parameters, abortController.signal); + + await act(async () => { + await waitForNextUpdate(); + }); + + expect(result.current.data).toEqual(response); + expect(result.current.isLoading).toEqual(false); + expect(result.current.error).toEqual(undefined); + }); + + it('should refetch with same parameters', async () => { + const { result, waitForNextUpdate } = renderUseFetch({ initialParameters: parameters }); + + expect(mockFetchFn).toHaveBeenCalledTimes(1); + expect(mockFetchFn).toHaveBeenCalledWith(parameters, abortController.signal); + + await act(async () => { + result.current.refetch(); + await waitForNextUpdate(); + }); + + expect(mockFetchFn).toHaveBeenCalledTimes(1); + expect(mockFetchFn).toHaveBeenCalledWith(parameters, abortController.signal); + }); + + it('should not call fetch if disabled option defined', async () => { + const { result, waitForNextUpdate } = renderUseFetch({ + initialParameters: parameters, + disabled: true, + }); + + expect(result.current.data).toEqual(undefined); + expect(result.current.isLoading).toEqual(true); + expect(result.current.error).toEqual(undefined); + + expect(mockFetchFn).not.toHaveBeenCalled(); + + await act(async () => { + result.current.fetch(parameters); + await waitForNextUpdate(); + }); + + expect(result.current.data).toEqual(undefined); + expect(result.current.isLoading).toEqual(true); + expect(result.current.error).toEqual(undefined); + expect(mockFetchFn).not.toHaveBeenCalled(); + }); + + it('should ignore state change if component is unmounted', async () => { + mockFetchFn.mockImplementationOnce(async () => { + unmount(); + return response; + }); + + const { result, waitForNextUpdate, unmount } = renderUseFetch(); + + expect(result.current.data).toEqual(undefined); + + await act(async () => { + result.current.fetch(parameters); + await waitForNextUpdate(); + }); + + expect(result.current.data).toEqual(undefined); + }); + + it('should ignore state change if error but component is unmounted', async () => { + mockFetchFn.mockImplementationOnce(async () => { + unmount(); + throw new Error(); + }); + + const { result, waitForNextUpdate, unmount } = renderUseFetch(); + + expect(result.current.error).toEqual(undefined); + + await act(async () => { + result.current.fetch(parameters); + await waitForNextUpdate(); + }); + + expect(result.current.error).toEqual(undefined); + }); + + it('should abort initial request if fetch is called', async () => { + const firstAbortCtrl = new AbortController(); + const abortSpy = jest.spyOn(window, 'AbortController').mockReturnValueOnce(firstAbortCtrl); + + const { result, waitForNextUpdate } = renderUseFetch({ initialParameters: parameters }); + + mockFetchFn.mockImplementationOnce(async () => { + result.current.fetch(parameters); + return response; + }); + + await act(async () => { + await waitForNextUpdate(); + }); + + expect(firstAbortCtrl.signal.aborted).toEqual(true); + + abortSpy.mockRestore(); + }); + + it('should abort first request if fetch is called twice', async () => { + const firstAbortCtrl = new AbortController(); + const abortSpy = jest.spyOn(window, 'AbortController').mockReturnValueOnce(firstAbortCtrl); + + const { result, waitForNextUpdate } = renderUseFetch(); + + mockFetchFn.mockImplementationOnce(async () => { + result.current.fetch(parameters); + return response; + }); + + await act(async () => { + result.current.fetch(parameters); + await waitForNextUpdate(); + }); + + expect(firstAbortCtrl.signal.aborted).toEqual(true); + + abortSpy.mockRestore(); + }); + + describe('APM tracking', () => { + it('should track with request name', async () => { + const { result, waitForNextUpdate } = renderUseFetch(); + + await act(async () => { + result.current.fetch(parameters); + await waitForNextUpdate(); + }); + + expect(mockStartTracking).toHaveBeenCalledTimes(1); + expect(mockStartTracking).toHaveBeenCalledWith({ name: requestName }); + }); + + it('should track each request', async () => { + const { result, waitForNextUpdate } = renderUseFetch({ + initialParameters: parameters, + }); + + await act(async () => { + await waitForNextUpdate(); + }); + + expect(mockFetchFn).toHaveBeenCalledTimes(1); + expect(mockStartTracking).toHaveBeenCalledTimes(1); + expect(mockEndTracking).toHaveBeenCalledTimes(1); + expect(mockStartTracking).toHaveBeenCalledWith({ name: requestName }); + + await act(async () => { + result.current.fetch(parameters); + await waitForNextUpdate(); + }); + + expect(mockFetchFn).toHaveBeenCalledTimes(2); + expect(mockStartTracking).toHaveBeenCalledTimes(2); + expect(mockEndTracking).toHaveBeenCalledTimes(2); + }); + + it('should end success', async () => { + const { result, waitForNextUpdate } = renderUseFetch(); + + await act(async () => { + result.current.fetch(parameters); + await waitForNextUpdate(); + }); + + expect(mockEndTracking).toHaveBeenCalledTimes(1); + expect(mockEndTracking).toHaveBeenCalledWith('success'); + }); + + it('should end aborted', async () => { + const abortCtrl = new AbortController(); + const abortSpy = jest.spyOn(window, 'AbortController').mockReturnValue(abortCtrl); + + mockFetchFn.mockImplementationOnce(async () => { + abortCtrl.abort(); + throw Error('request aborted'); + }); + + const { result, waitForNextUpdate } = renderUseFetch(); + + await act(async () => { + result.current.fetch(parameters); + await waitForNextUpdate(); + }); + + expect(mockEndTracking).toHaveBeenCalledTimes(1); + expect(mockEndTracking).toHaveBeenCalledWith('aborted'); + + abortSpy.mockRestore(); + }); + + it('should end error', async () => { + mockFetchFn.mockImplementationOnce(async () => { + throw Error('request error'); + }); + + const { result, waitForNextUpdate } = renderUseFetch(); + + await act(async () => { + result.current.fetch(parameters); + await waitForNextUpdate(); + }); + + expect(mockEndTracking).toHaveBeenCalledTimes(1); + expect(mockEndTracking).toHaveBeenCalledWith('error'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_fetch/use_fetch.ts b/x-pack/plugins/security_solution/public/common/hooks/use_fetch/use_fetch.ts new file mode 100644 index 00000000000000..330b47569b3e94 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_fetch/use_fetch.ts @@ -0,0 +1,169 @@ +/* + * 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 { useCallback, useEffect, useReducer } from 'react'; +import type { Reducer } from 'react'; +import { useTrackHttpRequest } from '../../lib/apm/use_track_http_request'; +import type { RequestName } from './request_names'; + +interface ResultState<Response, Error> { + /** + * The `data` will contain the raw response of the latest request executed. + * It is initialized to `undefined`. + */ + data?: Response; + isLoading: boolean; + /** + * The `error` will contain the error of the latest request executed. + * It is reset when a success response is completed. + */ + error?: Error; +} + +export interface Result<Parameters, Response, Error> extends ResultState<Response, Error> { + /** + * The `fetch` function starts a request with the parameters. + * It aborts any previous pending request and starts a new request, every time it is called. + * Optimizations are delegated to the consumer of the hook. + */ + fetch: (parameters: Parameters) => void; + /** + * The `refetch` function restarts a request with the latest parameters used. + * It aborts any previous pending request + */ + refetch: () => void; +} + +export type RequestFnParam<Parameters, Response> = ( + /** + * The parameters that will be passed to the fetch function provided. + */ + parameters: Parameters, + /** + * The abort signal. Call `signal.abort()` to abort the request. + */ + signal: AbortController['signal'] +) => Promise<Response>; + +export interface OptionsParam<Parameters> { + /** + * Disables the fetching and aborts any pending request when is set to `true`. + */ + disabled?: boolean; + /** + * Set `initialParameters` to start fetching immediately when the hook is called, without having to call the `fetch` function. + */ + initialParameters?: Parameters; +} + +interface State<Parameters, Response, Error> extends ResultState<Response, Error> { + parameters?: Parameters; +} + +type Action<Parameters, Response, Error> = + | { type: 'FETCH_INIT'; payload: Parameters } + | { type: 'FETCH_SUCCESS'; payload: Response } + | { type: 'FETCH_FAILURE'; payload?: Error } + | { type: 'FETCH_REPEAT' }; + +const requestReducer = <Parameters, Response, Error>( + state: State<Parameters, Response, Error>, + action: Action<Parameters, Response, Error> +): State<Parameters, Response, Error> => { + switch (action.type) { + case 'FETCH_INIT': + return { + ...state, + parameters: action.payload, + isLoading: true, + }; + case 'FETCH_SUCCESS': + return { + ...state, + data: action.payload, + isLoading: false, + error: undefined, + }; + case 'FETCH_FAILURE': + return { + ...state, + error: action.payload, + isLoading: false, + }; + case 'FETCH_REPEAT': + return { + ...state, + isLoading: true, + }; + default: + return state; + } +}; + +/** + * `useFetch` is a generic hook that simplifies the async request queries implementation. + * It provides: APM monitoring, abort control, error handling and refetching. + * @param requestName The unique name of the request. It is used for APM tracking, it should be descriptive. + * @param fetchFn The function provided to execute the fetch request. It should accept the request `parameters` and the abort `signal`. + * @param options Additional options. + */ +export const useFetch = <Parameters, Response, Error extends unknown>( + requestName: RequestName, + fetchFn: RequestFnParam<Parameters, Response>, + { disabled = false, initialParameters }: OptionsParam<Parameters> = {} +): Result<Parameters, Response, Error> => { + const { startTracking } = useTrackHttpRequest(); + + const [{ parameters, data, isLoading, error }, dispatch] = useReducer< + Reducer<State<Parameters, Response, Error>, Action<Parameters, Response, Error>> + >(requestReducer, { + data: undefined, + isLoading: initialParameters !== undefined, // isLoading state is used internally to control fetch executions + error: undefined, + parameters: initialParameters, + }); + + const fetch = useCallback( + (param: Parameters) => dispatch({ type: 'FETCH_INIT', payload: param }), + [] + ); + const refetch = useCallback(() => dispatch({ type: 'FETCH_REPEAT' }), []); + + useEffect(() => { + if (isLoading === false || parameters === undefined || disabled) { + return; + } + + let ignore = false; + const abortController = new AbortController(); + + const executeFetch = async () => { + const { endTracking } = startTracking({ name: requestName }); + try { + const response = await fetchFn(parameters, abortController.signal); + endTracking('success'); + if (!ignore) { + dispatch({ type: 'FETCH_SUCCESS', payload: response }); + } + } catch (err) { + endTracking(abortController.signal.aborted ? 'aborted' : 'error'); + if (!ignore) { + dispatch({ type: 'FETCH_FAILURE', payload: err }); + } + } + }; + + executeFetch(); + + return () => { + ignore = true; + abortController.abort(); + }; + }, [isLoading, parameters, disabled, fetchFn, startTracking, requestName]); + + return { fetch, refetch, data, isLoading, error }; +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/apm/user_actions.ts b/x-pack/plugins/security_solution/public/common/lib/apm/user_actions.ts index ba2a3fa77e637d..44703b4f5707cb 100644 --- a/x-pack/plugins/security_solution/public/common/lib/apm/user_actions.ts +++ b/x-pack/plugins/security_solution/public/common/lib/apm/user_actions.ts @@ -33,3 +33,21 @@ export const RULES_TABLE_ACTIONS = { PREVIEW_ON: `${APP_UI_ID} rulesTable technicalPreview on`, PREVIEW_OFF: `${APP_UI_ID} rulesTable technicalPreview off`, }; + +export const TIMELINE_ACTIONS = { + SAVE: `${APP_UI_ID} timeline save`, + DUPLICATE: `${APP_UI_ID} timeline duplicate`, // it includes duplicate template, create template from timeline and create timeline from template + DELETE: `${APP_UI_ID} timeline delete`, + BULK_DELETE: `${APP_UI_ID} timeline bulkDelete`, +}; + +export const ALERTS_ACTIONS = { + OPEN_ANALYZER: `${APP_UI_ID} alerts openAnalyzer`, + OPEN_SESSION_VIEW: `${APP_UI_ID} alerts openSessionView`, + INVESTIGATE_IN_TIMELINE: `${APP_UI_ID} alerts investigateInTimeline`, +}; + +export const FIELD_BROWSER_ACTIONS = { + FIELD_SAVED: `${APP_UI_ID} fieldBrowser fieldSaved`, + FIELD_DELETED: `${APP_UI_ID} fieldBrowser fieldDeleted`, +}; diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 00ca0e0a5852cd..d0d5fb0cd2cc5d 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -82,7 +82,7 @@ export const mockGlobalState: State = { hostRisk: { activePage: 0, limit: 10, - sort: { field: RiskScoreFields.riskScore, direction: Direction.desc }, + sort: { field: RiskScoreFields.hostRiskScore, direction: Direction.desc }, severitySelection: [], }, sessions: { activePage: 0, limit: 10 }, @@ -106,7 +106,7 @@ export const mockGlobalState: State = { hostRisk: { activePage: 0, limit: 10, - sort: { field: RiskScoreFields.riskScore, direction: Direction.desc }, + sort: { field: RiskScoreFields.hostRiskScore, direction: Direction.desc }, severitySelection: [], }, sessions: { activePage: 0, limit: 10 }, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.test.tsx index 5eebdd18acfd4b..f4b581060e1ef7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.test.tsx @@ -11,7 +11,6 @@ import { KibanaServices, useKibana } from '../../../../common/lib/kibana'; import type { Ecs } from '../../../../../common/ecs'; import * as actions from '../actions'; import { coreMock } from '@kbn/core/public/mocks'; -import type { SendAlertToTimelineActionProps } from '../types'; import { InvestigateInTimelineAction } from './investigate_in_timeline_action'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; @@ -30,9 +29,26 @@ const ecsRowData: Ecs = { }; jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../common/lib/apm/use_start_transaction'); jest.mock('../../../../common/hooks/use_app_toasts'); jest.mock('../actions'); +(KibanaServices.get as jest.Mock).mockReturnValue(coreMock.createStart()); +const mockSendAlertToTimeline = jest.spyOn(actions, 'sendAlertToTimelineAction'); +(useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + search: { + searchStrategyClient: jest.fn(), + }, + query: jest.fn(), + }, + }, +}); +(useAppToasts as jest.Mock).mockReturnValue({ + addError: jest.fn(), +}); + const props = { ecsRowData, onInvestigateInTimelineAlertClick: () => {}, @@ -40,28 +56,8 @@ const props = { }; describe('use investigate in timeline hook', () => { - let mockSendAlertToTimeline: jest.SpyInstance<Promise<void>, [SendAlertToTimelineActionProps]>; - - beforeEach(() => { - const coreStartMock = coreMock.createStart(); - (KibanaServices.get as jest.Mock).mockReturnValue(coreStartMock); - mockSendAlertToTimeline = jest.spyOn(actions, 'sendAlertToTimelineAction'); - (useKibana as jest.Mock).mockReturnValue({ - services: { - data: { - search: { - searchStrategyClient: jest.fn(), - }, - query: jest.fn(), - }, - }, - }); - (useAppToasts as jest.Mock).mockReturnValue({ - addError: jest.fn(), - }); - }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); test('it creates a component and click handler', () => { const wrapper = render( diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx index 4fd5ebc48e49b4..eaef9169925ae2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx @@ -12,7 +12,6 @@ import type { Ecs } from '../../../../../common/ecs'; import { useInvestigateInTimeline } from './use_investigate_in_timeline'; import * as actions from '../actions'; import { coreMock } from '@kbn/core/public/mocks'; -import type { SendAlertToTimelineActionProps } from '../types'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; const ecsRowData: Ecs = { @@ -30,37 +29,34 @@ const ecsRowData: Ecs = { }; jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../common/lib/apm/use_start_transaction'); jest.mock('../../../../common/hooks/use_app_toasts'); jest.mock('../actions'); +(KibanaServices.get as jest.Mock).mockReturnValue(coreMock.createStart()); +const mockSendAlertToTimeline = jest.spyOn(actions, 'sendAlertToTimelineAction'); +(useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + search: { + searchStrategyClient: jest.fn(), + }, + query: jest.fn(), + }, + }, +}); +(useAppToasts as jest.Mock).mockReturnValue({ + addError: jest.fn(), +}); + const props = { ecsRowData, onInvestigateInTimelineAlertClick: () => {}, }; describe('use investigate in timeline hook', () => { - let mockSendAlertToTimeline: jest.SpyInstance<Promise<void>, [SendAlertToTimelineActionProps]>; - - beforeEach(() => { - const coreStartMock = coreMock.createStart(); - (KibanaServices.get as jest.Mock).mockReturnValue(coreStartMock); - mockSendAlertToTimeline = jest.spyOn(actions, 'sendAlertToTimelineAction'); - (useKibana as jest.Mock).mockReturnValue({ - services: { - data: { - search: { - searchStrategyClient: jest.fn(), - }, - query: jest.fn(), - }, - }, - }); - (useAppToasts as jest.Mock).mockReturnValue({ - addError: jest.fn(), - }); - }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); test('it creates a component and click handler', () => { const { result } = renderHook(() => useInvestigateInTimeline(props), { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx index ce5b7ee9c5de53..8d5eb34fe580ff 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx @@ -30,6 +30,8 @@ import { ACTION_INVESTIGATE_IN_TIMELINE } from '../translations'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { getField } from '../../../../helpers'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; +import { ALERTS_ACTIONS } from '../../../../common/lib/apm/user_actions'; interface UseInvestigateInTimelineActionProps { ecsRowData?: Ecs | Ecs[] | null; @@ -45,6 +47,7 @@ export const useInvestigateInTimeline = ({ data: { search: searchStrategyClient, query }, } = useKibana().services; const dispatch = useDispatch(); + const { startTransaction } = useStartTransaction(); const { services } = useKibana(); const { getExceptionListsItems } = useApi(services.http); @@ -141,6 +144,7 @@ export const useInvestigateInTimeline = ({ ); const investigateInTimelineAlertClick = useCallback(async () => { + startTransaction({ name: ALERTS_ACTIONS.INVESTIGATE_IN_TIMELINE }); if (onInvestigateInTimelineAlertClick) { onInvestigateInTimelineAlertClick(); } @@ -154,6 +158,7 @@ export const useInvestigateInTimeline = ({ }); } }, [ + startTransaction, createTimeline, ecsRowData, onInvestigateInTimelineAlertClick, diff --git a/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx index 126f057742901d..4999d757cd0479 100644 --- a/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx @@ -25,16 +25,19 @@ const OsqueryActionWrapper = styled.div` `; export interface OsqueryFlyoutProps { - agentId: string; + agentId?: string; + defaultValues?: {}; onClose: () => void; } -const TimelineComponent = React.memo((props) => { - return <EuiButtonEmpty {...props} size="xs" />; -}); +const TimelineComponent = React.memo((props) => <EuiButtonEmpty {...props} size="xs" />); TimelineComponent.displayName = 'TimelineComponent'; -export const OsqueryFlyoutComponent: React.FC<OsqueryFlyoutProps> = ({ agentId, onClose }) => { +export const OsqueryFlyoutComponent: React.FC<OsqueryFlyoutProps> = ({ + agentId, + defaultValues, + onClose, +}) => { const { services: { osquery, timelines }, } = useKibana(); @@ -70,30 +73,38 @@ export const OsqueryFlyoutComponent: React.FC<OsqueryFlyoutProps> = ({ agentId, }, [getAddToTimelineButton] ); - // @ts-expect-error - const { OsqueryAction } = osquery; - return ( - <EuiFlyout - ownFocus - maskProps={{ style: 'z-index: 5000' }} // For an edge case to display above the timeline flyout - size="m" - onClose={onClose} - > - <EuiFlyoutHeader hasBorder data-test-subj="flyout-header-osquery"> - <EuiTitle> - <h2>{ACTION_OSQUERY}</h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <OsqueryActionWrapper data-test-subj="flyout-body-osquery"> - <OsqueryAction agentId={agentId} formType="steps" addToTimeline={handleAddToTimeline} /> - </OsqueryActionWrapper> - </EuiFlyoutBody> - <EuiFlyoutFooter> - <OsqueryEventDetailsFooter handleClick={onClose} data-test-subj="flyout-footer-osquery" /> - </EuiFlyoutFooter> - </EuiFlyout> - ); + + if (osquery?.OsqueryAction) { + return ( + <EuiFlyout + ownFocus + maskProps={{ style: 'z-index: 5000' }} // For an edge case to display above the timeline flyout + size="m" + onClose={onClose} + > + <EuiFlyoutHeader hasBorder data-test-subj="flyout-header-osquery"> + <EuiTitle> + <h2>{ACTION_OSQUERY}</h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <OsqueryActionWrapper data-test-subj="flyout-body-osquery"> + <osquery.OsqueryAction + agentId={agentId} + formType="steps" + defaultValues={defaultValues} + addToTimeline={handleAddToTimeline} + /> + </OsqueryActionWrapper> + </EuiFlyoutBody> + <EuiFlyoutFooter> + <OsqueryEventDetailsFooter handleClick={onClose} data-test-subj="flyout-footer-osquery" /> + </EuiFlyoutFooter> + </EuiFlyout> + ); + } + + return null; }; export const OsqueryFlyout = React.memo(OsqueryFlyoutComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/helpers.test.ts index deee988052ef27..8433dbb2fbf876 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/helpers.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import moment from 'moment'; import { DataSourceType } from '../../../pages/detection_engine/rules/types'; import { isNoisy, @@ -14,51 +15,75 @@ import { } from './helpers'; describe('query_preview/helpers', () => { + const timeframeEnd = moment(); + const startHourAgo = timeframeEnd.clone().subtract(1, 'hour'); + const startDayAgo = timeframeEnd.clone().subtract(1, 'day'); + const startMonthAgo = timeframeEnd.clone().subtract(1, 'month'); + + const lastHourTimeframe = { + timeframeStart: startHourAgo, + timeframeEnd, + interval: '5m', + lookback: '1m', + }; + const lastDayTimeframe = { + timeframeStart: startDayAgo, + timeframeEnd, + interval: '1h', + lookback: '5m', + }; + const lastMonthTimeframe = { + timeframeStart: startMonthAgo, + timeframeEnd, + interval: '1d', + lookback: '1h', + }; + describe('isNoisy', () => { test('returns true if timeframe selection is "Last hour" and average hits per hour is greater than one execution duration', () => { - const isItNoisy = isNoisy(30, 'h'); + const isItNoisy = isNoisy(30, lastHourTimeframe); expect(isItNoisy).toBeTruthy(); }); test('returns false if timeframe selection is "Last hour" and average hits per hour is less than one execution duration', () => { - const isItNoisy = isNoisy(0, 'h'); + const isItNoisy = isNoisy(0, lastHourTimeframe); expect(isItNoisy).toBeFalsy(); }); test('returns true if timeframe selection is "Last day" and average hits per hour is greater than one execution duration', () => { - const isItNoisy = isNoisy(50, 'd'); + const isItNoisy = isNoisy(50, lastDayTimeframe); expect(isItNoisy).toBeTruthy(); }); test('returns false if timeframe selection is "Last day" and average hits per hour is equal to one execution duration', () => { - const isItNoisy = isNoisy(24, 'd'); + const isItNoisy = isNoisy(24, lastDayTimeframe); expect(isItNoisy).toBeFalsy(); }); test('returns false if timeframe selection is "Last day" and hits is 0', () => { - const isItNoisy = isNoisy(0, 'd'); + const isItNoisy = isNoisy(0, lastDayTimeframe); expect(isItNoisy).toBeFalsy(); }); test('returns true if timeframe selection is "Last month" and average hits per hour is greater than one execution duration', () => { - const isItNoisy = isNoisy(50, 'M'); + const isItNoisy = isNoisy(750, lastMonthTimeframe); expect(isItNoisy).toBeTruthy(); }); test('returns false if timeframe selection is "Last month" and average hits per hour is equal to one execution duration', () => { - const isItNoisy = isNoisy(30, 'M'); + const isItNoisy = isNoisy(30, lastMonthTimeframe); expect(isItNoisy).toBeFalsy(); }); test('returns false if timeframe selection is "Last month" and hits is 0', () => { - const isItNoisy = isNoisy(0, 'M'); + const isItNoisy = isNoisy(0, lastMonthTimeframe); expect(isItNoisy).toBeFalsy(); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/helpers.ts index 266c0185745af2..6acd2e4db8f947 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/helpers.ts @@ -9,7 +9,6 @@ import { isEmpty } from 'lodash'; import { Position, ScaleType } from '@elastic/charts'; import type { EuiSelectOption } from '@elastic/eui'; import type { Type, Language, ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types'; -import type { Unit } from '@kbn/datemath'; import type { Filter } from '@kbn/es-query'; import * as i18n from './translations'; import { histogramDateTimeFormatter } from '../../../../common/components/utils'; @@ -17,6 +16,7 @@ import type { ChartSeriesConfigs } from '../../../../common/components/charts/co import { getQueryFilter } from '../../../../../common/detection_engine/get_query_filter'; import type { FieldValueQueryBar } from '../query_bar'; import type { ESQuery } from '../../../../../common/typed_json'; +import type { TimeframePreviewOptions } from '../../../pages/detection_engine/rules/types'; import { DataSourceType } from '../../../pages/detection_engine/rules/types'; /** @@ -25,18 +25,13 @@ import { DataSourceType } from '../../../pages/detection_engine/rules/types'; * @param hits Total query search hits * @param timeframe Range selected by user (last hour, day...) */ -export const isNoisy = (hits: number, timeframe: Unit): boolean => { - if (timeframe === 'h') { - return hits > 1; - } else if (timeframe === 'd') { - return hits / 24 > 1; - } else if (timeframe === 'w') { - return hits / 168 > 1; - } else if (timeframe === 'M') { - return hits / 30 > 1; - } - - return false; +export const isNoisy = (hits: number, timeframe: TimeframePreviewOptions): boolean => { + const oneHour = 1000 * 60 * 60; + const durationInHours = Math.max( + (timeframe.timeframeEnd.valueOf() - timeframe.timeframeStart.valueOf()) / oneHour, + 1.0 + ); + return hits / durationInHours > 1; }; /** diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.test.tsx index 3b635796edd64b..de0d2458a5cfa6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.test.tsx @@ -7,17 +7,22 @@ import React from 'react'; import { render } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import type { DataViewBase } from '@kbn/es-query'; import { fields } from '@kbn/data-plugin/common/mocks'; import { TestProviders } from '../../../../common/mock'; import type { RulePreviewProps } from '.'; -import { RulePreview } from '.'; +import { RulePreview, REASONABLE_INVOCATION_COUNT } from '.'; import { usePreviewRoute } from './use_preview_route'; import { usePreviewHistogram } from './use_preview_histogram'; import { DataSourceType } from '../../../pages/detection_engine/rules/types'; +import { + getStepScheduleDefaultValue, + stepAboutDefaultValue, + stepDefineDefaultValue, +} from '../../../pages/detection_engine/rules/utils'; +import { usePreviewInvocationCount } from '../../../containers/detection_engine/rules/use_preview_invocation_count'; jest.mock('../../../../common/lib/kibana'); jest.mock('./use_preview_route'); @@ -30,6 +35,7 @@ jest.mock('../../../../common/containers/use_global_time', () => ({ setQuery: jest.fn(), }), })); +jest.mock('../../../containers/detection_engine/rules/use_preview_invocation_count'); const getMockIndexPattern = (): DataViewBase => ({ fields, @@ -38,42 +44,46 @@ const getMockIndexPattern = (): DataViewBase => ({ }); const defaultProps: RulePreviewProps = { - ruleType: 'threat_match', - index: ['test-*'], - indexPattern: getMockIndexPattern(), - dataSourceType: DataSourceType.IndexPatterns, - threatIndex: ['threat-*'], - threatMapping: [ - { - entries: [ - { field: 'file.hash.md5', value: 'threat.indicator.file.hash.md5', type: 'mapping' }, - ], + defineRuleData: { + ...stepDefineDefaultValue, + ruleType: 'threat_match', + index: ['test-*'], + indexPattern: getMockIndexPattern(), + dataSourceType: DataSourceType.IndexPatterns, + threatIndex: ['threat-*'], + threatMapping: [ + { + entries: [ + { field: 'file.hash.md5', value: 'threat.indicator.file.hash.md5', type: 'mapping' }, + ], + }, + ], + queryBar: { + filters: [], + query: { query: 'file.hash.md5:*', language: 'kuery' }, + saved_id: null, }, - ], - isDisabled: false, - query: { - filters: [], - query: { query: 'file.hash.md5:*', language: 'kuery' }, - saved_id: null, - }, - threatQuery: { - filters: [], - query: { query: 'threat.indicator.file.hash.md5:*', language: 'kuery' }, - saved_id: null, - }, - threshold: { - field: ['agent.hostname'], - value: '200', - cardinality: { - field: ['user.name'], - value: '2', + threatQueryBar: { + filters: [], + query: { query: 'threat.indicator.file.hash.md5:*', language: 'kuery' }, + saved_id: null, + }, + threshold: { + field: ['agent.hostname'], + value: '200', + cardinality: { + field: ['user.name'], + value: '2', + }, }, + anomalyThreshold: 50, + machineLearningJobId: ['test-ml-job-id'], + eqlOptions: {}, + newTermsFields: ['host.ip'], + historyWindowSize: '7d', }, - anomalyThreshold: 50, - machineLearningJobId: ['test-ml-job-id'], - eqlOptions: {}, - newTermsFields: ['host.ip'], - historyWindowSize: '7d', + aboutRuleData: stepAboutDefaultValue, + scheduleRuleData: getStepScheduleDefaultValue('threat_match'), }; describe('PreviewQuery', () => { @@ -98,6 +108,8 @@ describe('PreviewQuery', () => { isPreviewRequestInProgress: false, previewId: undefined, }); + + (usePreviewInvocationCount as jest.Mock).mockReturnValue({ invocationCount: 500 }); }); afterEach(() => { @@ -115,26 +127,6 @@ describe('PreviewQuery', () => { expect(await wrapper.findByTestId('preview-time-frame')).toBeTruthy(); }); - test('it renders preview button disabled if "isDisabled" is true', async () => { - const wrapper = render( - <TestProviders> - <RulePreview {...defaultProps} isDisabled={true} /> - </TestProviders> - ); - - expect(await wrapper.getByTestId('queryPreviewButton').closest('button')).toBeDisabled(); - }); - - test('it renders preview button enabled if "isDisabled" is false', async () => { - const wrapper = render( - <TestProviders> - <RulePreview {...defaultProps} /> - </TestProviders> - ); - - expect(await wrapper.getByTestId('queryPreviewButton').closest('button')).not.toBeDisabled(); - }); - test('does not render histogram when there is no previewId', async () => { const wrapper = render( <TestProviders> @@ -145,40 +137,9 @@ describe('PreviewQuery', () => { expect(await wrapper.queryByTestId('[data-test-subj="preview-histogram-panel"]')).toBeNull(); }); - test('it renders quick/advanced query toggle button', async () => { - const wrapper = render( - <TestProviders> - <RulePreview {...defaultProps} /> - </TestProviders> - ); - - expect(await wrapper.findByTestId('quickAdvancedToggleButtonGroup')).toBeTruthy(); - }); - - test('it renders timeframe, interval and look-back buttons when advanced query is selected', async () => { - const wrapper = render( - <TestProviders> - <RulePreview {...defaultProps} /> - </TestProviders> - ); - - expect(await wrapper.findByTestId('quickAdvancedToggleButtonGroup')).toBeTruthy(); - const advancedQueryButton = await wrapper.findByTestId('advancedQuery'); - userEvent.click(advancedQueryButton); - expect(await wrapper.findByTestId('detectionEnginePreviewRuleInterval')).toBeTruthy(); - expect(await wrapper.findByTestId('detectionEnginePreviewRuleLookback')).toBeTruthy(); - }); - - test('it renders invocation count warning when advanced query is selected and warning flag is set to true', async () => { - (usePreviewRoute as jest.Mock).mockReturnValue({ - hasNoiseWarning: false, - addNoiseWarning: jest.fn(), - createPreview: jest.fn(), - clearPreview: jest.fn(), - logs: [], - isPreviewRequestInProgress: false, - previewId: undefined, - showInvocationCountWarning: true, + test('it renders invocation count warning when invocation count is bigger then "REASONABLE_INVOCATION_COUNT"', async () => { + (usePreviewInvocationCount as jest.Mock).mockReturnValue({ + invocationCount: REASONABLE_INVOCATION_COUNT + 1, }); const wrapper = render( @@ -187,8 +148,6 @@ describe('PreviewQuery', () => { </TestProviders> ); - const advancedQueryButton = await wrapper.findByTestId('advancedQuery'); - userEvent.click(advancedQueryButton); expect(await wrapper.findByTestId('previewInvocationCountWarning')).toBeTruthy(); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx index 0639fda39ca4da..a5532e176b3c33 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx @@ -5,53 +5,38 @@ * 2.0. */ -import React, { useState, useEffect, useMemo, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import dateMath from '@kbn/datemath'; -import type { Unit } from '@kbn/datemath'; -import type { ThreatMapping, Type } from '@kbn/securitysolution-io-ts-alerting-types'; -import styled from 'styled-components'; -import type { DataViewBase } from '@kbn/es-query'; -import type { EuiButtonGroupOptionProps, OnTimeChangeProps } from '@elastic/eui'; +import type { OnTimeChangeProps } from '@elastic/eui'; import { - EuiButtonGroup, EuiCallOut, EuiFlexGroup, EuiFlexItem, - EuiSelect, EuiFormRow, - EuiButton, EuiSpacer, EuiSuperDatePicker, + EuiSuperUpdateButton, } from '@elastic/eui'; import moment from 'moment'; -import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs'; -import type { FieldValueQueryBar } from '../query_bar'; +import type { List } from '@kbn/securitysolution-io-ts-list-types'; +import { isEqual } from 'lodash'; import * as i18n from './translations'; import { usePreviewRoute } from './use_preview_route'; import { PreviewHistogram } from './preview_histogram'; -import { getTimeframeOptions } from './helpers'; import { PreviewLogsComponent } from './preview_logs'; import { useKibana } from '../../../../common/lib/kibana'; import { LoadingHistogram } from './loading_histogram'; -import type { FieldValueThreshold } from '../threshold_input'; -import { isJobStarted } from '../../../../../common/machine_learning/helpers'; -import type { EqlOptionsSelected } from '../../../../../common/search_strategy'; import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; import { SINGLE_RULE_ACTIONS } from '../../../../common/lib/apm/user_actions'; -import { Form, UseField, useForm, useFormData } from '../../../../shared_imports'; -import { ScheduleItem } from '../schedule_item_form'; import type { - AdvancedPreviewForm, - DataSourceType, + AboutStepRule, + DefineStepRule, + ScheduleStepRule, + TimeframePreviewOptions, } from '../../../pages/detection_engine/rules/types'; -import { schema } from './schema'; +import { usePreviewInvocationCount } from '../../../containers/detection_engine/rules/use_preview_invocation_count'; -const HelpTextComponent = ( - <EuiFlexGroup direction="column" gutterSize="none"> - <EuiFlexItem>{i18n.QUERY_PREVIEW_HELP_TEXT}</EuiFlexItem> - <EuiFlexItem>{i18n.QUERY_PREVIEW_DISCLAIMER}</EuiFlexItem> - </EuiFlexGroup> -); +export const REASONABLE_INVOCATION_COUNT = 200; const timeRanges = [ { start: 'now/d', end: 'now', label: 'Today' }, @@ -64,42 +49,20 @@ const timeRanges = [ { start: 'now-30d', end: 'now', label: 'Last 30 days' }, ]; -const QUICK_QUERY_SELECT_ID = 'quickQuery'; -const ADVANCED_QUERY_SELECT_ID = 'advancedQuery'; - -const advancedOptionsDefaultValue = { - interval: '5m', - lookback: '1m', -}; - export interface RulePreviewProps { - index: string[]; - indexPattern: DataViewBase; - isDisabled: boolean; - query: FieldValueQueryBar; - dataViewId?: string; - dataSourceType: DataSourceType; - ruleType: Type; - threatIndex: string[]; - threatMapping: ThreatMapping; - threatQuery: FieldValueQueryBar; - threshold: FieldValueThreshold; - machineLearningJobId: string[]; - anomalyThreshold: number; - eqlOptions: EqlOptionsSelected; - newTermsFields: string[]; - historyWindowSize: string; + isDisabled?: boolean; + defineRuleData: DefineStepRule; + aboutRuleData: AboutStepRule; + scheduleRuleData: ScheduleStepRule; + exceptionsList?: List[]; } -const Select = styled(EuiSelect)` - width: ${({ theme }) => theme.eui.euiSuperDatePickerWidth}; -`; - -const PreviewButton = styled(EuiButton)` - margin-left: 0; -`; - -const defaultTimeRange: Unit = 'h'; +interface RulePreviewState { + defineRuleData?: DefineStepRule; + aboutRuleData?: AboutStepRule; + scheduleRuleData?: ScheduleStepRule; + timeframeOptions: TimeframePreviewOptions; +} const refreshedTimeframe = (startDate: string, endDate: string) => { return { @@ -109,25 +72,14 @@ const refreshedTimeframe = (startDate: string, endDate: string) => { }; const RulePreviewComponent: React.FC<RulePreviewProps> = ({ - index, - indexPattern, - dataViewId, - dataSourceType, isDisabled, - query, - ruleType, - threatIndex, - threatQuery, - threatMapping, - threshold, - machineLearningJobId, - anomalyThreshold, - eqlOptions, - newTermsFields, - historyWindowSize, + defineRuleData, + aboutRuleData, + scheduleRuleData, + exceptionsList, }) => { + const { indexPattern, ruleType } = defineRuleData; const { spaces } = useKibana().services; - const { loading: isMlLoading, jobs } = useSecurityJobs(false); const [spaceId, setSpaceId] = useState(''); useEffect(() => { @@ -144,107 +96,50 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({ const [timeframeStart, setTimeframeStart] = useState(moment().subtract(1, 'hour')); const [timeframeEnd, setTimeframeEnd] = useState(moment()); + const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false); + useEffect(() => { const { start, end } = refreshedTimeframe(startDate, endDate); setTimeframeStart(start); setTimeframeEnd(end); }, [startDate, endDate]); - const { form } = useForm<AdvancedPreviewForm>({ - defaultValue: advancedOptionsDefaultValue, - options: { stripEmptyFields: false }, - schema, + // The data state that we used for the last preview results + const [previewData, setPreviewData] = useState<RulePreviewState>({ + timeframeOptions: { + timeframeStart, + timeframeEnd, + interval: '5m', + lookback: '1m', + }, }); - const [{ interval: formInterval, lookback: formLookback }] = useFormData<AdvancedPreviewForm>({ - form, - watch: ['interval', 'lookback'], + const { invocationCount } = usePreviewInvocationCount({ + timeframeOptions: { + timeframeStart, + timeframeEnd, + interval: scheduleRuleData.interval, + lookback: scheduleRuleData.from, + }, }); + const showInvocationCountWarning = invocationCount > REASONABLE_INVOCATION_COUNT; - const areRelaventMlJobsRunning = useMemo(() => { - if (ruleType !== 'machine_learning') { - return true; // Don't do the expensive logic if we don't need it - } - if (isMlLoading) { - return false; - } - const selectedJobs = jobs.filter(({ id }) => machineLearningJobId.includes(id)); - return selectedJobs.every((job) => isJobStarted(job.jobState, job.datafeedState)); - }, [jobs, machineLearningJobId, ruleType, isMlLoading]); - - const [queryPreviewIdSelected, setQueryPreviewRadioIdSelected] = useState(QUICK_QUERY_SELECT_ID); - - // Callback for when user toggles between Quick query and Advanced query preview - const onChangeDataSource = (optionId: string) => { - setQueryPreviewRadioIdSelected(optionId); - }; - - const quickAdvancedToggleButtonOptions: EuiButtonGroupOptionProps[] = useMemo( - () => [ - { - id: QUICK_QUERY_SELECT_ID, - label: i18n.QUICK_PREVIEW_TOGGLE_BUTTON, - 'data-test-subj': `rule-preview-toggle-${QUICK_QUERY_SELECT_ID}`, - }, - { - id: ADVANCED_QUERY_SELECT_ID, - label: i18n.ADVANCED_PREVIEW_TOGGLE_BUTTON, - 'data-test-subj': `rule-index-toggle-${ADVANCED_QUERY_SELECT_ID}`, - }, - ], - [] - ); - - const showAdvancedOptions = queryPreviewIdSelected === ADVANCED_QUERY_SELECT_ID; - const advancedOptions = useMemo( - () => - showAdvancedOptions && formInterval && formLookback - ? { - timeframeStart, - timeframeEnd, - interval: formInterval, - lookback: formLookback, - } - : undefined, - [formInterval, formLookback, showAdvancedOptions, timeframeEnd, timeframeStart] - ); - - const [timeFrame, setTimeFrame] = useState<Unit>(defaultTimeRange); const { addNoiseWarning, createPreview, - clearPreview, isPreviewRequestInProgress, previewId, logs, hasNoiseWarning, isAborted, - showInvocationCountWarning, } = usePreviewRoute({ - index, - isDisabled, - dataViewId, - dataSourceType, - query, - threatIndex, - threatQuery, - timeFrame, - ruleType, - threatMapping, - threshold, - machineLearningJobId, - anomalyThreshold, - eqlOptions, - newTermsFields, - historyWindowSize, - advancedOptions, + defineRuleData: previewData.defineRuleData, + aboutRuleData: previewData.aboutRuleData, + scheduleRuleData: previewData.scheduleRuleData, + exceptionsList, + timeframeOptions: previewData.timeframeOptions, }); - // Resets the timeFrame to default when rule type is changed because not all time frames are supported by all rule types - useEffect(() => { - setTimeFrame(defaultTimeRange); - }, [ruleType]); - const { startTransaction } = useStartTransaction(); const [isRefreshing, setIsRefreshing] = useState(false); @@ -256,21 +151,15 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({ setIsRefreshing(false); }, [isRefreshing, createPreview]); - const handlePreviewClick = useCallback(() => { - startTransaction({ name: SINGLE_RULE_ACTIONS.PREVIEW }); - if (showAdvancedOptions) { - // Refresh timeframe on Preview button click to make sure that relative times recalculated based on current time - const { start, end } = refreshedTimeframe(startDate, endDate); - setTimeframeStart(start); - setTimeframeEnd(end); - } else { - clearPreview(); - } - setIsRefreshing(true); - }, [clearPreview, endDate, showAdvancedOptions, startDate, startTransaction]); + useEffect(() => { + const { start, end } = refreshedTimeframe(startDate, endDate); + setTimeframeStart(start); + setTimeframeEnd(end); + }, [endDate, startDate]); const onTimeChange = useCallback( ({ start: newStart, end: newEnd, isInvalid }: OnTimeChangeProps) => { + setIsDateRangeInvalid(isInvalid); if (!isInvalid) { setStartDate(newStart); setEndDate(newEnd); @@ -279,18 +168,50 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({ [] ); + const onTimeframeRefresh = useCallback(() => { + startTransaction({ name: SINGLE_RULE_ACTIONS.PREVIEW }); + const { start, end } = refreshedTimeframe(startDate, endDate); + setTimeframeStart(start); + setTimeframeEnd(end); + setPreviewData({ + defineRuleData, + aboutRuleData, + scheduleRuleData, + timeframeOptions: { + timeframeStart: start, + timeframeEnd: end, + interval: scheduleRuleData.interval, + lookback: scheduleRuleData.from, + }, + }); + setIsRefreshing(true); + }, [aboutRuleData, defineRuleData, endDate, scheduleRuleData, startDate, startTransaction]); + + const isDirty = useMemo( + () => + !timeframeStart.isSame(previewData.timeframeOptions.timeframeStart) || + !timeframeEnd.isSame(previewData.timeframeOptions.timeframeEnd) || + !isEqual(defineRuleData, previewData.defineRuleData) || + !isEqual(aboutRuleData, previewData.aboutRuleData) || + !isEqual(scheduleRuleData, previewData.scheduleRuleData), + [ + aboutRuleData, + defineRuleData, + previewData.aboutRuleData, + previewData.defineRuleData, + previewData.scheduleRuleData, + previewData.timeframeOptions.timeframeEnd, + previewData.timeframeOptions.timeframeStart, + scheduleRuleData, + timeframeEnd, + timeframeStart, + ] + ); + return ( <> - <EuiButtonGroup - legend="Quick query or advanced query preview selector" - data-test-subj="quickAdvancedToggleButtonGroup" - idSelected={queryPreviewIdSelected} - onChange={onChangeDataSource} - options={quickAdvancedToggleButtonOptions} - color="primary" - /> <EuiSpacer size="s" /> - {showAdvancedOptions && showInvocationCountWarning && ( + {showInvocationCountWarning && ( <> <EuiCallOut color="warning" @@ -304,83 +225,44 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({ )} <EuiFormRow label={i18n.QUERY_PREVIEW_LABEL} - helpText={HelpTextComponent} error={undefined} isInvalid={false} data-test-subj="rule-preview" describedByIds={['rule-preview']} > - <EuiFlexGroup> - <EuiFlexItem grow={1}> - {showAdvancedOptions ? ( - <EuiSuperDatePicker - start={startDate} - end={endDate} - onTimeChange={onTimeChange} - showUpdateButton={false} - isDisabled={isDisabled} - commonlyUsedRanges={timeRanges} - /> - ) : ( - <Select - id="preview-time-frame" - options={getTimeframeOptions(ruleType)} - value={timeFrame} - onChange={(e) => setTimeFrame(e.target.value as Unit)} - aria-label={i18n.QUERY_PREVIEW_SELECT_ARIA} - disabled={isDisabled} - data-test-subj="preview-time-frame" - /> - )} - </EuiFlexItem> + <EuiFlexGroup alignItems="center" responsive={false} gutterSize="s"> + <EuiSuperDatePicker + start={startDate} + end={endDate} + isDisabled={isDisabled} + onTimeChange={onTimeChange} + showUpdateButton={false} + commonlyUsedRanges={timeRanges} + onRefresh={onTimeframeRefresh} + data-test-subj="preview-time-frame" + /> <EuiFlexItem grow={false}> - <PreviewButton - fill - isLoading={isPreviewRequestInProgress} - isDisabled={isDisabled || !areRelaventMlJobsRunning} - onClick={handlePreviewClick} - data-test-subj="queryPreviewButton" - > - {i18n.QUERY_PREVIEW_BUTTON} - </PreviewButton> + <EuiSuperUpdateButton + isDisabled={isDateRangeInvalid || isDisabled} + iconType={isDirty ? 'kqlFunction' : 'refresh'} + onClick={onTimeframeRefresh} + color={isDirty ? 'success' : 'primary'} + fill={true} + data-test-subj="previewSubmitButton" + /> </EuiFlexItem> </EuiFlexGroup> </EuiFormRow> - {showAdvancedOptions && ( - <Form form={form} data-test-subj="previewRule"> - <EuiSpacer size="s" /> - <UseField - path="interval" - component={ScheduleItem} - componentProps={{ - idAria: 'detectionEnginePreviewRuleInterval', - isDisabled, - dataTestSubj: 'detectionEnginePreviewRuleInterval', - }} - /> - <UseField - path="lookback" - component={ScheduleItem} - componentProps={{ - idAria: 'detectionEnginePreviewRuleLookback', - isDisabled, - dataTestSubj: 'detectionEnginePreviewRuleLookback', - minimumValue: 1, - }} - /> - <EuiSpacer size="s" /> - </Form> - )} + <EuiSpacer size="l" /> {isPreviewRequestInProgress && <LoadingHistogram />} {!isPreviewRequestInProgress && previewId && spaceId && ( <PreviewHistogram ruleType={ruleType} - timeFrame={timeFrame} previewId={previewId} addNoiseWarning={addNoiseWarning} spaceId={spaceId} indexPattern={indexPattern} - advancedOptions={advancedOptions} + timeframeOptions={previewData.timeframeOptions} /> )} <PreviewLogsComponent logs={logs} hasNoiseWarning={hasNoiseWarning} isAborted={isAborted} /> diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/loading_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/loading_histogram.tsx index bb370d94ed7f1c..6f7e60c175300d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/loading_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/loading_histogram.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer, EuiLoadingChart } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiLoadingChart } from '@elastic/eui'; import styled from 'styled-components'; import * as i18n from './translations'; import { Panel } from '../../../../common/components/panel'; @@ -29,14 +29,7 @@ export const LoadingHistogram = () => { <EuiFlexItem grow={1}> <LoadingChart size="l" data-test-subj="preview-histogram-loading" /> </EuiFlexItem> - <EuiFlexItem grow={false}> - <> - <EuiSpacer /> - <EuiText size="s" color="subdued"> - <p>{i18n.QUERY_PREVIEW_DISCLAIMER}</p> - </EuiText> - </> - </EuiFlexItem> + <EuiSpacer /> </EuiFlexGroup> </Panel> ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx index e9fde44db0b441..024a1bdea18106 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx @@ -30,6 +30,13 @@ const getMockIndexPattern = (): DataViewBase => ({ title: 'logstash-*', }); +const getLastMonthTimeframe = () => ({ + timeframeStart: moment().subtract(1, 'month'), + timeframeEnd: moment(), + interval: '5m', + lookback: '1m', +}); + describe('PreviewHistogram', () => { const mockSetQuery = jest.fn(); @@ -63,7 +70,7 @@ describe('PreviewHistogram', () => { <TestProviders> <PreviewHistogram addNoiseWarning={jest.fn()} - timeFrame="M" + timeframeOptions={getLastMonthTimeframe()} previewId={'test-preview-id'} spaceId={'default'} ruleType={'query'} @@ -94,7 +101,7 @@ describe('PreviewHistogram', () => { <TestProviders> <PreviewHistogram addNoiseWarning={jest.fn()} - timeFrame="M" + timeframeOptions={getLastMonthTimeframe()} previewId={'test-preview-id'} spaceId={'default'} ruleType={'query'} @@ -146,12 +153,11 @@ describe('PreviewHistogram', () => { <TestProviders> <PreviewHistogram addNoiseWarning={jest.fn()} - timeFrame="M" previewId={'test-preview-id'} spaceId={'default'} ruleType={'query'} indexPattern={getMockIndexPattern()} - advancedOptions={{ + timeframeOptions={{ timeframeStart: moment(start, format), timeframeEnd: moment(end, format), interval: '5m', diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx index 589e11c17016ac..278919555303c4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx @@ -7,7 +7,6 @@ import React, { useEffect, useMemo } from 'react'; import usePrevious from 'react-use/lib/usePrevious'; -import type { Unit } from '@kbn/datemath'; import { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer, EuiLoadingChart } from '@elastic/eui'; import styled from 'styled-components'; import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; @@ -27,7 +26,6 @@ import { Panel } from '../../../../common/components/panel'; import { HeaderSection } from '../../../../common/components/header_section'; import { BarChart } from '../../../../common/components/charts/barchart'; import { usePreviewHistogram } from './use_preview_histogram'; -import { formatDate } from '../../../../common/components/super_date_picker'; import { alertsPreviewDefaultModel } from '../../alerts_table/default_config'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { defaultRowRenderers } from '../../../../timelines/components/timeline/body/renderers'; @@ -42,7 +40,7 @@ import { useGlobalFullScreen } from '../../../../common/containers/use_full_scre import { InspectButtonContainer } from '../../../../common/components/inspect'; import { timelineActions } from '../../../../timelines/store/timeline'; import type { State } from '../../../../common/store'; -import type { AdvancedPreviewOptions } from '../../../pages/detection_engine/rules/types'; +import type { TimeframePreviewOptions } from '../../../pages/detection_engine/rules/types'; const LoadingChart = styled(EuiLoadingChart)` display: block; @@ -59,39 +57,32 @@ const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>` export const ID = 'previewHistogram'; interface PreviewHistogramProps { - timeFrame: Unit; previewId: string; addNoiseWarning: () => void; spaceId: string; ruleType: Type; - indexPattern: DataViewBase; - advancedOptions?: AdvancedPreviewOptions; + indexPattern: DataViewBase | undefined; + timeframeOptions: TimeframePreviewOptions; } const DEFAULT_HISTOGRAM_HEIGHT = 300; export const PreviewHistogram = ({ - timeFrame, previewId, addNoiseWarning, spaceId, ruleType, indexPattern, - advancedOptions, + timeframeOptions, }: PreviewHistogramProps) => { const dispatch = useDispatch(); const { setQuery, isInitializing } = useGlobalTime(); const { timelines: timelinesUi } = useKibana().services; - const from = useMemo(() => `now-1${timeFrame}`, [timeFrame]); - const to = useMemo(() => 'now', []); const startDate = useMemo( - () => (advancedOptions ? advancedOptions.timeframeStart.toISOString() : formatDate(from)), - [from, advancedOptions] - ); - const endDate = useMemo( - () => (advancedOptions ? advancedOptions.timeframeEnd.toISOString() : formatDate(to)), - [to, advancedOptions] + () => timeframeOptions.timeframeStart.toISOString(), + [timeframeOptions] ); + const endDate = useMemo(() => timeframeOptions.timeframeEnd.toISOString(), [timeframeOptions]); const isEqlRule = useMemo(() => ruleType === 'eql', [ruleType]); const isMlRule = useMemo(() => ruleType === 'machine_learning', [ruleType]); @@ -133,11 +124,11 @@ export const PreviewHistogram = ({ useEffect(() => { if (previousPreviewId !== previewId && totalCount > 0) { - if (isNoisy(totalCount, timeFrame)) { + if (isNoisy(totalCount, timeframeOptions)) { addNoiseWarning(); } } - }, [totalCount, addNoiseWarning, timeFrame, previousPreviewId, previewId]); + }, [totalCount, addNoiseWarning, previousPreviewId, previewId, timeframeOptions]); useEffect((): void => { if (!isLoading && !isInitializing) { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/schema.tsx deleted file mode 100644 index 85521359ed25db..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/schema.tsx +++ /dev/null @@ -1,43 +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. - */ - -/* istanbul ignore file */ - -import { i18n } from '@kbn/i18n'; - -import { OptionalFieldLabel } from '../optional_field_label'; -import type { AdvancedPreviewForm } from '../../../pages/detection_engine/rules/types'; -import type { FormSchema } from '../../../../shared_imports'; - -export const schema: FormSchema<AdvancedPreviewForm> = { - interval: { - label: i18n.translate('xpack.securitySolution.detectionEngine.previewRule.fieldIntervalLabel', { - defaultMessage: 'Runs every (Rule interval)', - }), - helpText: i18n.translate( - 'xpack.securitySolution.detectionEngine.previewRule.fieldIntervalHelpText', - { - defaultMessage: 'Rules run periodically and detect alerts within the specified time frame.', - } - ), - }, - lookback: { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.previewRule.fieldAdditionalLookBackLabel', - { - defaultMessage: 'Additional look-back time', - } - ), - labelAppend: OptionalFieldLabel, - helpText: i18n.translate( - 'xpack.securitySolution.detectionEngine.previewRule.fieldAdditionalLookBackHelpText', - { - defaultMessage: 'Adds time to the look-back period to prevent missed alerts.', - } - ), - }, -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/translations.ts index 42db4d75c4951d..aff8b7dd37ac7e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/translations.ts @@ -61,14 +61,7 @@ export const QUERY_PREVIEW_SELECT_ARIA = i18n.translate( export const QUERY_PREVIEW_LABEL = i18n.translate( 'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel', { - defaultMessage: 'Timeframe', - } -); - -export const QUERY_PREVIEW_HELP_TEXT = i18n.translate( - 'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText', - { - defaultMessage: 'Select a timeframe of data to preview query results.', + defaultMessage: 'Select a preview timeframe', } ); @@ -115,14 +108,6 @@ export const QUERY_PREVIEW_ERROR = i18n.translate( } ); -export const QUERY_PREVIEW_DISCLAIMER = i18n.translate( - 'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewDisclaimer', - { - defaultMessage: - 'Note: This preview excludes effects of rule exceptions and timestamp overrides.', - } -); - export const PREVIEW_HISTOGRAM_DISCLAIMER = i18n.translate( 'xpack.securitySolution.detectionEngine.queryPreview.histogramDisclaimer', { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_histogram.tsx index 16f57dc4022117..a0c6e706c19ef9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_histogram.tsx @@ -21,7 +21,7 @@ interface PreviewHistogramParams { startDate: string; spaceId: string; ruleType: Type; - indexPattern: DataViewBase; + indexPattern: DataViewBase | undefined; } export const usePreviewHistogram = ({ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_route.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_route.tsx index 7c5c3e673fd2af..92d6cc63c323c3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_route.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_route.tsx @@ -5,80 +5,37 @@ * 2.0. */ -import { useEffect, useMemo, useState, useCallback } from 'react'; -import moment from 'moment'; -import type { Unit } from '@kbn/datemath'; -import type { Type, ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types'; -import type { FieldValueQueryBar } from '../query_bar'; +import { useEffect, useState, useCallback } from 'react'; +import type { List } from '@kbn/securitysolution-io-ts-list-types'; import { usePreviewRule } from '../../../containers/detection_engine/rules/use_preview_rule'; import { formatPreviewRule } from '../../../pages/detection_engine/rules/create/helpers'; -import type { FieldValueThreshold } from '../threshold_input'; import type { RulePreviewLogs } from '../../../../../common/detection_engine/schemas/request'; -import type { EqlOptionsSelected } from '../../../../../common/search_strategy'; import type { - AdvancedPreviewOptions, - DataSourceType, + AboutStepRule, + DefineStepRule, + ScheduleStepRule, + TimeframePreviewOptions, } from '../../../pages/detection_engine/rules/types'; interface PreviewRouteParams { - isDisabled: boolean; - index: string[]; - dataViewId?: string; - dataSourceType: DataSourceType; - threatIndex: string[]; - query: FieldValueQueryBar; - threatQuery: FieldValueQueryBar; - ruleType: Type; - timeFrame: Unit; - threatMapping: ThreatMapping; - threshold: FieldValueThreshold; - machineLearningJobId: string[]; - anomalyThreshold: number; - eqlOptions: EqlOptionsSelected; - newTermsFields: string[]; - historyWindowSize: string; - advancedOptions?: AdvancedPreviewOptions; + defineRuleData?: DefineStepRule; + aboutRuleData?: AboutStepRule; + scheduleRuleData?: ScheduleStepRule; + exceptionsList?: List[]; + timeframeOptions: TimeframePreviewOptions; } export const usePreviewRoute = ({ - index, - dataViewId, - dataSourceType, - isDisabled, - query, - threatIndex, - threatQuery, - timeFrame, - ruleType, - threatMapping, - threshold, - machineLearningJobId, - anomalyThreshold, - eqlOptions, - newTermsFields, - historyWindowSize, - advancedOptions, + defineRuleData, + aboutRuleData, + scheduleRuleData, + exceptionsList, + timeframeOptions, }: PreviewRouteParams) => { const [isRequestTriggered, setIsRequestTriggered] = useState(false); - const [timeframeEnd, setTimeframeEnd] = useState(moment()); - useEffect(() => { - if (isRequestTriggered) { - setTimeframeEnd(moment()); - } - }, [isRequestTriggered, setTimeframeEnd]); - - const quickQueryOptions = useMemo( - () => ({ - timeframe: timeFrame, - timeframeEnd, - }), - [timeFrame, timeframeEnd] - ); - - const { isLoading, showInvocationCountWarning, response, rule, setRule } = usePreviewRule({ - quickQueryOptions, - advancedOptions, + const { isLoading, response, rule, setRule } = usePreviewRule({ + timeframeOptions, }); const [logs, setLogs] = useState<RulePreviewLogs[]>(response.logs ?? []); const [isAborted, setIsAborted] = useState<boolean>(!!response.isAborted); @@ -102,69 +59,27 @@ export const usePreviewRoute = ({ }, [setRule]); useEffect(() => { - clearPreview(); - }, [ - clearPreview, - index, - isDisabled, - query, - threatIndex, - threatQuery, - timeFrame, - ruleType, - threatMapping, - threshold, - machineLearningJobId, - anomalyThreshold, - eqlOptions, - newTermsFields, - historyWindowSize, - advancedOptions, - ]); - - useEffect(() => { + if (!defineRuleData || !aboutRuleData || !scheduleRuleData) { + return; + } if (isRequestTriggered && rule === null) { setRule( formatPreviewRule({ - index, - dataViewId, - dataSourceType, - query, - ruleType, - threatIndex, - threatMapping, - threatQuery, - timeFrame, - threshold, - machineLearningJobId, - anomalyThreshold, - eqlOptions, - newTermsFields, - historyWindowSize, - advancedOptions, + defineRuleData, + aboutRuleData, + scheduleRuleData, + exceptionsList, }) ); } }, [ - index, - dataViewId, - dataSourceType, isRequestTriggered, - query, rule, - ruleType, setRule, - threatIndex, - threatMapping, - threatQuery, - timeFrame, - threshold, - machineLearningJobId, - anomalyThreshold, - eqlOptions, - newTermsFields, - historyWindowSize, - advancedOptions, + defineRuleData, + aboutRuleData, + scheduleRuleData, + exceptionsList, ]); return { @@ -176,6 +91,5 @@ export const usePreviewRoute = ({ previewId: response.previewId ?? '', logs, isAborted, - showInvocationCountWarning, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index af610bc2a17be1..34303c00e51537 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -11,6 +11,7 @@ import React, { memo, useCallback, useEffect, useState, useMemo } from 'react'; import styled from 'styled-components'; import type { DataViewBase } from '@kbn/es-query'; +import { isThreatMatchRule } from '../../../../../common/detection_engine/utils'; import type { RuleStepProps, AboutStepRule, @@ -31,7 +32,6 @@ import { } from '../../../../shared_imports'; import { defaultRiskScoreBySeverity, severityOptions } from './data'; -import { stepAboutDefaultValue } from './default_value'; import { isUrlInvalid } from '../../../../common/utils/validators'; import { schema as defaultSchema, threatIndicatorPathRequiredSchemaValue } from './schema'; import * as I18n from './translations'; @@ -42,7 +42,6 @@ import { SeverityField } from '../severity_mapping'; import { RiskScoreField } from '../risk_score_mapping'; import { AutocompleteField } from '../autocomplete_field'; import { useFetchIndex } from '../../../../common/containers/source'; -import { isThreatMatchRule } from '../../../../../common/detection_engine/utils'; import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants'; import { useKibana } from '../../../../common/lib/kibana'; import { useRuleIndices } from '../../../containers/detection_engine/rules/use_rule_indices'; @@ -50,8 +49,9 @@ import { useRuleIndices } from '../../../containers/detection_engine/rules/use_r const CommonUseField = getUseField({ component: Field }); interface StepAboutRuleProps extends RuleStepProps { - defaultValues?: AboutStepRule; + defaultValues: AboutStepRule; defineRuleData?: DefineStepRule; + onRuleDataChange?: (data: AboutStepRule) => void; } const ThreeQuartersContainer = styled.div` @@ -68,7 +68,7 @@ TagContainer.displayName = 'TagContainer'; const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({ addPadding = false, - defaultValues, + defaultValues: initialState, defineRuleData, descriptionColumns = 'singleSplit', isReadOnlyView, @@ -76,21 +76,13 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({ isLoading, onSubmit, setForm, + onRuleDataChange, }) => { const { data } = useKibana().services; const isThreatMatchRuleValue = useMemo( () => isThreatMatchRule(defineRuleData?.ruleType), - [defineRuleData?.ruleType] - ); - - const initialState: AboutStepRule = useMemo( - () => - defaultValues ?? - (isThreatMatchRuleValue - ? { ...stepAboutDefaultValue, threatIndicatorPath: DEFAULT_INDICATOR_SOURCE_PATH } - : stepAboutDefaultValue), - [defaultValues, isThreatMatchRuleValue] + [defineRuleData] ); const schema = useMemo( @@ -147,7 +139,25 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({ const [{ severity: formSeverity, timestampOverride: formTimestampOverride }] = useFormData<AboutStepRule>({ form, - watch: ['severity', 'timestampOverride'], + watch: [ + 'isAssociatedToEndpointList', + 'isBuildingBlock', + 'riskScore', + 'ruleNameOverride', + 'severity', + 'timestampOverride', + 'threat', + 'timestampOverrideFallbackDisabled', + ], + onChange: (aboutData: AboutStepRule) => { + if (onRuleDataChange) { + onRuleDataChange({ + threatIndicatorPath: undefined, + timestampOverrideFallbackDisabled: undefined, + ...aboutData, + }); + } + }, }); useEffect(() => { @@ -166,7 +176,14 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({ const getData = useCallback(async () => { const result = await submit(); return result?.isValid - ? result + ? { + isValid: true, + data: { + threatIndicatorPath: undefined, + timestampOverrideFallbackDisabled: undefined, + ...result.data, + }, + } : { isValid: false, data: getFormData(), diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.test.tsx index 5df5e79e16a22a..4e41ac0f996eac 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { StepDefineRule, aggregatableFields } from '.'; +import { stepDefineDefaultValue } from '../../../pages/detection_engine/rules/utils'; jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../common/hooks/use_selector', () => { @@ -84,7 +85,15 @@ test('aggregatableFields with aggregatable: true', function () { describe('StepDefineRule', () => { it('renders correctly', () => { - const wrapper = shallow(<StepDefineRule isReadOnlyView={false} isLoading={false} />); + const wrapper = shallow( + <StepDefineRule + isReadOnlyView={false} + isLoading={false} + indicesConfig={[]} + threatIndicesConfig={[]} + defaultValues={stepDefineDefaultValue} + /> + ); expect(wrapper.find('Form[data-test-subj="stepDefineRule"]')).toHaveLength(1); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index f9be710725986f..b2a3075928feee 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -26,17 +26,11 @@ import usePrevious from 'react-use/lib/usePrevious'; import type { DataViewBase } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n-react'; -import { - DEFAULT_INDEX_KEY, - DEFAULT_THREAT_INDEX_KEY, - DEFAULT_THREAT_MATCH_QUERY, -} from '../../../../../common/constants'; -import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions'; import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities'; -import { useUiSetting$, useKibana } from '../../../../common/lib/kibana'; +import { useKibana } from '../../../../common/lib/kibana'; import type { EqlOptionsSelected, FieldsEqlOptions } from '../../../../../common/search_strategy'; import { filterRuleFieldsForType, @@ -75,12 +69,12 @@ import { DataViewSelector } from '../data_view_selector'; import { ThreatMatchInput } from '../threatmatch_input'; import type { BrowserField } from '../../../../common/containers/source'; import { useFetchIndex } from '../../../../common/containers/source'; -import { RulePreview } from '../rule_preview'; -import { getIsRulePreviewDisabled } from '../rule_preview/helpers'; import { NewTermsFields } from '../new_terms_fields'; import { ScheduleItem } from '../schedule_item_form'; import { DocLink } from '../../../../common/components/links_to_docs/doc_link'; import { StepDefineRuleNewFeaturesTour } from './new_features_tour'; +import { defaultCustomQuery } from '../../../pages/detection_engine/rules/utils'; +import { getIsRulePreviewDisabled } from '../rule_preview/helpers'; const CommonUseField = getUseField({ component: Field }); @@ -88,62 +82,13 @@ const StyledVisibleContainer = styled.div<{ isVisible: boolean }>` display: ${(props) => (props.isVisible ? 'block' : 'none')}; `; interface StepDefineRuleProps extends RuleStepProps { - defaultValues?: DefineStepRule; + indicesConfig: string[]; + threatIndicesConfig: string[]; + defaultValues: DefineStepRule; + onRuleDataChange?: (data: DefineStepRule) => void; + onPreviewDisabledStateChange?: (isDisabled: boolean) => void; } -export const stepDefineDefaultValue: DefineStepRule = { - anomalyThreshold: 50, - index: [], - machineLearningJobId: [], - ruleType: 'query', - threatIndex: [], - queryBar: { - query: { query: '', language: 'kuery' }, - filters: [], - saved_id: null, - }, - threatQueryBar: { - query: { query: DEFAULT_THREAT_MATCH_QUERY, language: 'kuery' }, - filters: [], - saved_id: null, - }, - requiredFields: [], - relatedIntegrations: [], - threatMapping: [], - threshold: { - field: [], - value: '200', - cardinality: { - field: [], - value: '', - }, - }, - timeline: { - id: null, - title: DEFAULT_TIMELINE_TITLE, - }, - eqlOptions: {}, - dataSourceType: DataSourceType.IndexPatterns, - newTermsFields: [], - historyWindowSize: '7d', -}; - -/** - * This default query will be used for threat query/indicator matches - * as the default when the user swaps to using it by changing their - * rule type from any rule type to the "threatMatchRule" type. Only - * difference is that "*:*" is used instead of '' for its query. - */ -const threatQueryBarDefaultValue: DefineStepRule['queryBar'] = { - ...stepDefineDefaultValue.queryBar, - query: { ...stepDefineDefaultValue.queryBar.query, query: '*:*' }, -}; - -const defaultCustomQuery = { - forNormalRules: stepDefineDefaultValue.queryBar, - forThreatMatchRules: threatQueryBarDefaultValue, -}; - export const MyLabelButton = styled(EuiButtonEmpty)` height: 18px; font-size: 12px; @@ -166,7 +111,7 @@ const RuleTypeEuiFormRow = styled(EuiFormRow).attrs<{ $isVisible: boolean }>(({ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({ addPadding = false, - defaultValues, + defaultValues: initialState, descriptionColumns = 'singleSplit', isReadOnlyView, isLoading, @@ -174,6 +119,10 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({ onSubmit, setForm, kibanaDataViews, + indicesConfig, + threatIndicesConfig, + onRuleDataChange, + onPreviewDisabledStateChange, }) => { const mlCapabilities = useMlCapabilities(); const [openTimelineSearch, setOpenTimelineSearch] = useState(false); @@ -181,14 +130,6 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({ const [threatIndexModified, setThreatIndexModified] = useState(false); const [dataViewTitle, setDataViewTitle] = useState<string>(); - const [indicesConfig] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); - const [threatIndicesConfig] = useUiSetting$<string[]>(DEFAULT_THREAT_INDEX_KEY); - const initialState = defaultValues ?? { - ...stepDefineDefaultValue, - index: indicesConfig, - threatIndex: threatIndicesConfig, - }; - const { form } = useForm<DefineStepRule>({ defaultValue: initialState, options: { stripEmptyFields: false }, @@ -196,23 +137,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({ }); const { getFields, getFormData, reset, submit } = form; - const [ - { - index: formIndex, - ruleType: formRuleType, - queryBar: formQuery, - dataViewId: formDataViewId, - threatIndex: formThreatIndex, - threatQueryBar: formThreatQuery, - threshold: formThreshold, - threatMapping: formThreatMapping, - machineLearningJobId: formMachineLearningJobId, - anomalyThreshold: formAnomalyThreshold, - dataSourceType: formDataSourceType, - newTermsFields: formNewTermsFields, - historyWindowSize: formHistoryWindowSize, - }, - ] = useFormData<DefineStepRule>({ + const [formData] = useFormData<DefineStepRule>({ form, watch: [ 'index', @@ -232,25 +157,77 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({ 'newTermsFields', 'historyWindowSize', ], + onChange: (data: DefineStepRule) => { + if (onRuleDataChange) { + onRuleDataChange({ + ...data, + eqlOptions: optionsSelected, + }); + } + }, }); + const { + index: formIndex, + ruleType: formRuleType, + queryBar: formQuery, + dataViewId: formDataViewId, + threatIndex: formThreatIndex, + threatMapping: formThreatMapping, + machineLearningJobId: formMachineLearningJobId, + dataSourceType: formDataSourceType, + newTermsFields: formNewTermsFields, + } = formData; const [isQueryBarValid, setIsQueryBarValid] = useState(false); const [isThreatQueryBarValid, setIsThreatQueryBarValid] = useState(false); const index = formIndex || initialState.index; const dataView = formDataViewId || initialState.dataViewId; const threatIndex = formThreatIndex || initialState.threatIndex; - const machineLearningJobId = formMachineLearningJobId ?? initialState.machineLearningJobId; - const anomalyThreshold = formAnomalyThreshold ?? initialState.anomalyThreshold; - const newTermsFields = formNewTermsFields ?? initialState.newTermsFields; - const historyWindowSize = formHistoryWindowSize ?? initialState.historyWindowSize; const ruleType = formRuleType || initialState.ruleType; const dataSourceType = formDataSourceType || initialState.dataSourceType; + const machineLearningJobId = formMachineLearningJobId ?? initialState.machineLearningJobId; + + const [isPreviewValid, setIsPreviewValid] = useState(false); + useEffect(() => { + if (onPreviewDisabledStateChange) { + onPreviewDisabledStateChange(!isPreviewValid); + } + }, [isPreviewValid, onPreviewDisabledStateChange]); + useEffect(() => { + const isDisabled = getIsRulePreviewDisabled({ + ruleType, + isQueryBarValid, + isThreatQueryBarValid, + index, + dataViewId: formDataViewId, + dataSourceType, + threatIndex, + threatMapping: formThreatMapping, + machineLearningJobId, + queryBar: formQuery ?? initialState.queryBar, + newTermsFields: formNewTermsFields, + }); + setIsPreviewValid(!isDisabled); + }, [ + dataSourceType, + formDataViewId, + formNewTermsFields, + formQuery, + formThreatMapping, + index, + initialState.queryBar, + isQueryBarValid, + isThreatQueryBarValid, + machineLearningJobId, + ruleType, + threatIndex, + ]); // if 'index' is selected, use these browser fields // otherwise use the dataview browserfields const previousRuleType = usePrevious(ruleType); const [optionsSelected, setOptionsSelected] = useState<EqlOptionsSelected>( - defaultValues?.eqlOptions || {} + initialState.eqlOptions || {} ); const [isIndexPatternLoading, { browserFields, indexPatterns: initIndexPattern }] = useFetchIndex( index, @@ -836,39 +813,6 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({ }} /> </Form> - <EuiSpacer size="m" /> - <RuleTypeEuiFormRow label={i18n.RULE_PREVIEW_TITLE} $isVisible={true} fullWidth> - <RulePreview - index={index} - indexPattern={indexPattern} - dataViewId={formDataViewId} - dataSourceType={dataSourceType} - isDisabled={getIsRulePreviewDisabled({ - ruleType, - isQueryBarValid, - isThreatQueryBarValid, - index, - dataViewId: formDataViewId, - dataSourceType, - threatIndex, - threatMapping: formThreatMapping, - machineLearningJobId, - queryBar: formQuery ?? initialState.queryBar, - newTermsFields: formNewTermsFields, - })} - query={formQuery} - ruleType={ruleType} - threatIndex={threatIndex} - threatQuery={formThreatQuery} - threatMapping={formThreatMapping} - threshold={formThreshold} - machineLearningJobId={machineLearningJobId} - anomalyThreshold={anomalyThreshold} - eqlOptions={optionsSelected} - newTermsFields={newTermsFields} - historyWindowSize={historyWindowSize} - /> - </RuleTypeEuiFormRow> </StepContentWrapper> {!isUpdateView && ( diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.test.tsx index 83141e7d886a85..310cb06ad93ce6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.test.tsx @@ -10,18 +10,32 @@ import { shallow, mount } from 'enzyme'; import { TestProviders } from '../../../../common/mock'; import { StepScheduleRule } from '.'; +import { getStepScheduleDefaultValue } from '../../../pages/detection_engine/rules/utils'; describe('StepScheduleRule', () => { it('renders correctly', () => { - const wrapper = mount(<StepScheduleRule isReadOnlyView={false} isLoading={false} />, { - wrappingComponent: TestProviders, - }); + const wrapper = mount( + <StepScheduleRule + isReadOnlyView={false} + isLoading={false} + defaultValues={getStepScheduleDefaultValue('query')} + />, + { + wrappingComponent: TestProviders, + } + ); expect(wrapper.find('Form[data-test-subj="stepScheduleRule"]')).toHaveLength(1); }); it('renders correctly if isReadOnlyView', () => { - const wrapper = shallow(<StepScheduleRule isReadOnlyView={true} isLoading={false} />); + const wrapper = shallow( + <StepScheduleRule + isReadOnlyView={true} + isLoading={false} + defaultValues={getStepScheduleDefaultValue('query')} + /> + ); expect(wrapper.find('StepContentWrapper')).toHaveLength(1); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx index 4695a665f72af8..d28e99864c1d49 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx @@ -7,48 +7,32 @@ import type { FC } from 'react'; import React, { memo, useCallback, useEffect } from 'react'; -import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import type { RuleStepProps, ScheduleStepRule } from '../../../pages/detection_engine/rules/types'; import { RuleStep } from '../../../pages/detection_engine/rules/types'; import { StepRuleDescription } from '../description_step'; import { ScheduleItem } from '../schedule_item_form'; -import { Form, UseField, useForm } from '../../../../shared_imports'; +import { Form, UseField, useForm, useFormData } from '../../../../shared_imports'; import { StepContentWrapper } from '../step_content_wrapper'; -import { isThreatMatchRule } from '../../../../../common/detection_engine/utils'; import { NextStep } from '../next_step'; import { schema } from './schema'; interface StepScheduleRuleProps extends RuleStepProps { - defaultValues?: ScheduleStepRule | null; - ruleType?: Type; + defaultValues: ScheduleStepRule; + onRuleDataChange?: (data: ScheduleStepRule) => void; } -const DEFAULT_INTERVAL = '5m'; -const DEFAULT_FROM = '1m'; -const THREAT_MATCH_INTERVAL = '1h'; -const THREAT_MATCH_FROM = '5m'; - -const getStepScheduleDefaultValue = (ruleType: Type | undefined): ScheduleStepRule => { - return { - interval: isThreatMatchRule(ruleType) ? THREAT_MATCH_INTERVAL : DEFAULT_INTERVAL, - from: isThreatMatchRule(ruleType) ? THREAT_MATCH_FROM : DEFAULT_FROM, - }; -}; - const StepScheduleRuleComponent: FC<StepScheduleRuleProps> = ({ addPadding = false, - defaultValues, + defaultValues: initialState, descriptionColumns = 'singleSplit', isReadOnlyView, isLoading, isUpdateView = false, onSubmit, setForm, - ruleType, + onRuleDataChange, }) => { - const initialState = defaultValues ?? getStepScheduleDefaultValue(ruleType); - const { form } = useForm<ScheduleStepRule>({ defaultValue: initialState, options: { stripEmptyFields: false }, @@ -57,6 +41,16 @@ const StepScheduleRuleComponent: FC<StepScheduleRuleProps> = ({ const { getFormData, submit } = form; + useFormData<ScheduleStepRule>({ + form, + watch: ['from', 'interval'], + onChange: (data: ScheduleStepRule) => { + if (onRuleDataChange) { + onRuleDataChange(data); + } + }, + }); + const handleSubmit = useCallback(() => { if (onSubmit) { onSubmit(); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/threatmatch_input/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/threatmatch_input/index.tsx index 7ee8d014f2ab4f..003268cedd0ccf 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/threatmatch_input/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/threatmatch_input/index.tsx @@ -34,7 +34,7 @@ interface ThreatMatchInputProps { threatIndexPatternsLoading: boolean; threatIndexModified: boolean; handleResetThreatIndices: () => void; - onValidityChange: (isValid: boolean) => void; + onValidityChange?: (isValid: boolean) => void; } const ThreatMatchInputComponent: React.FC<ThreatMatchInputProps> = ({ @@ -54,7 +54,9 @@ const ThreatMatchInputComponent: React.FC<ThreatMatchInputProps> = ({ const [isThreatIndexPatternValid, setIsThreatIndexPatternValid] = useState(false); useEffect(() => { - onValidityChange(!isThreatMappingInvalid && isThreatIndexPatternValid); + if (onValidityChange) { + onValidityChange(!isThreatMappingInvalid && isThreatIndexPatternValid); + } }, [isThreatIndexPatternValid, isThreatMappingInvalid, onValidityChange]); const handleBuilderOnChange = useCallback( diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts index 69aa2a4502bc03..e445e5b935af2a 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts @@ -5,10 +5,8 @@ * 2.0. */ -import type { - GetInstalledIntegrationsResponse, - RulesSchema, -} from '../../../../../../common/detection_engine/schemas/response'; +import type { FullResponseSchema } from '../../../../../../common/detection_engine/schemas/request'; +import type { GetInstalledIntegrationsResponse } from '../../../../../../common/detection_engine/schemas/response'; import { getRulesSchemaMock } from '../../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; import { savedRuleMock, rulesMock } from '../mock'; @@ -25,14 +23,16 @@ import type { FetchRulesProps, } from '../types'; -export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise<RulesSchema> => +export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise<FullResponseSchema> => Promise.resolve(getRulesSchemaMock()); -export const createRule = async ({ rule, signal }: CreateRulesProps): Promise<RulesSchema> => +export const createRule = async ({ rule, signal }: CreateRulesProps): Promise<FullResponseSchema> => Promise.resolve(getRulesSchemaMock()); -export const patchRule = async ({ ruleProperties, signal }: PatchRuleProps): Promise<RulesSchema> => - Promise.resolve(getRulesSchemaMock()); +export const patchRule = async ({ + ruleProperties, + signal, +}: PatchRuleProps): Promise<FullResponseSchema> => Promise.resolve(getRulesSchemaMock()); export const getPrePackagedRulesStatus = async ({ signal, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index f2e78eeee99ef3..63754adc5e7c80 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -25,7 +25,6 @@ import type { PreviewResponse, } from '../../../../../common/detection_engine/schemas/request'; import type { - RulesSchema, GetInstalledIntegrationsResponse, RulesReferencedByExceptionListsSchema, } from '../../../../../common/detection_engine/schemas/response'; @@ -74,8 +73,8 @@ export const createRule = async ({ rule, signal }: CreateRulesProps): Promise<Fu * * @throws An error if response is not OK */ -export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise<RulesSchema> => - KibanaServices.get().http.fetch<RulesSchema>(DETECTION_ENGINE_RULES_URL, { +export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise<FullResponseSchema> => + KibanaServices.get().http.fetch<FullResponseSchema>(DETECTION_ENGINE_RULES_URL, { method: 'PUT', body: JSON.stringify(rule), signal, @@ -92,8 +91,11 @@ export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise<Ru * * @throws An error if response is not OK */ -export const patchRule = async ({ ruleProperties, signal }: PatchRuleProps): Promise<RulesSchema> => - KibanaServices.get().http.fetch<RulesSchema>(DETECTION_ENGINE_RULES_URL, { +export const patchRule = async ({ + ruleProperties, + signal, +}: PatchRuleProps): Promise<FullResponseSchema> => + KibanaServices.get().http.fetch<FullResponseSchema>(DETECTION_ENGINE_RULES_URL, { method: 'PATCH', body: JSON.stringify(ruleProperties), signal, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_preview_invocation_count.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_preview_invocation_count.ts new file mode 100644 index 00000000000000..fa6cb829808327 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_preview_invocation_count.ts @@ -0,0 +1,36 @@ +/* + * 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 moment from 'moment'; + +import type { TimeframePreviewOptions } from '../../../pages/detection_engine/rules/types'; +import { getTimeTypeValue } from '../../../pages/detection_engine/rules/create/helpers'; + +export const usePreviewInvocationCount = ({ + timeframeOptions, +}: { + timeframeOptions: TimeframePreviewOptions; +}) => { + const timeframeDuration = + (timeframeOptions.timeframeEnd.valueOf() / 1000 - + timeframeOptions.timeframeStart.valueOf() / 1000) * + 1000; + + const { unit: intervalUnit, value: intervalValue } = getTimeTypeValue(timeframeOptions.interval); + const duration = moment.duration(intervalValue, intervalUnit); + const ruleIntervalDuration = duration.asMilliseconds(); + + const invocationCount = Math.max(Math.ceil(timeframeDuration / ruleIntervalDuration), 1); + const interval = timeframeOptions.interval; + + const { unit: lookbackUnit, value: lookbackValue } = getTimeTypeValue(timeframeOptions.lookback); + duration.add(lookbackValue, lookbackUnit); + + const from = `now-${duration.asSeconds()}s`; + + return { invocationCount, interval, from }; +}; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_preview_rule.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_preview_rule.ts index ed7f4150f7349e..224b69665117e8 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_preview_rule.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_preview_rule.ts @@ -6,13 +6,7 @@ */ import { useEffect, useMemo, useState } from 'react'; -import moment from 'moment'; -import { - RULE_PREVIEW_FROM, - RULE_PREVIEW_INTERVAL, - RULE_PREVIEW_INVOCATION_COUNT, -} from '../../../../../common/detection_engine/constants'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import type { PreviewResponse, @@ -22,13 +16,8 @@ import type { import { previewRule } from './api'; import * as i18n from './translations'; import { transformOutput } from './transforms'; -import type { - AdvancedPreviewOptions, - QuickQueryPreviewOptions, -} from '../../../pages/detection_engine/rules/types'; -import { getTimeTypeValue } from '../../../pages/detection_engine/rules/create/helpers'; - -const REASONABLE_INVOCATION_COUNT = 200; +import type { TimeframePreviewOptions } from '../../../pages/detection_engine/rules/types'; +import { usePreviewInvocationCount } from './use_preview_invocation_count'; const emptyPreviewRule: PreviewResponse = { previewId: undefined, @@ -37,65 +26,21 @@ const emptyPreviewRule: PreviewResponse = { }; export const usePreviewRule = ({ - quickQueryOptions, - advancedOptions, + timeframeOptions, }: { - quickQueryOptions: QuickQueryPreviewOptions; - advancedOptions?: AdvancedPreviewOptions; + timeframeOptions: TimeframePreviewOptions; }) => { const [rule, setRule] = useState<CreateRulesSchema | null>(null); const [response, setResponse] = useState<PreviewResponse>(emptyPreviewRule); const [isLoading, setIsLoading] = useState(false); const { addError } = useAppToasts(); - let invocationCount = RULE_PREVIEW_INVOCATION_COUNT.HOUR; - let interval: string = RULE_PREVIEW_INTERVAL.HOUR; - let from: string = RULE_PREVIEW_FROM.HOUR; + const { invocationCount, interval, from } = usePreviewInvocationCount({ timeframeOptions }); - switch (quickQueryOptions.timeframe) { - case 'd': - invocationCount = RULE_PREVIEW_INVOCATION_COUNT.DAY; - interval = RULE_PREVIEW_INTERVAL.DAY; - from = RULE_PREVIEW_FROM.DAY; - break; - case 'w': - invocationCount = RULE_PREVIEW_INVOCATION_COUNT.WEEK; - interval = RULE_PREVIEW_INTERVAL.WEEK; - from = RULE_PREVIEW_FROM.WEEK; - break; - case 'M': - invocationCount = RULE_PREVIEW_INVOCATION_COUNT.MONTH; - interval = RULE_PREVIEW_INTERVAL.MONTH; - from = RULE_PREVIEW_FROM.MONTH; - break; - } const timeframeEnd = useMemo( - () => - advancedOptions - ? advancedOptions.timeframeEnd.toISOString() - : quickQueryOptions.timeframeEnd.toISOString(), - [advancedOptions, quickQueryOptions] + () => timeframeOptions.timeframeEnd.toISOString(), + [timeframeOptions] ); - if (advancedOptions) { - const timeframeDuration = - (advancedOptions.timeframeEnd.valueOf() / 1000 - - advancedOptions.timeframeStart.valueOf() / 1000) * - 1000; - - const { unit: intervalUnit, value: intervalValue } = getTimeTypeValue(advancedOptions.interval); - const duration = moment.duration(intervalValue, intervalUnit); - const ruleIntervalDuration = duration.asMilliseconds(); - - invocationCount = Math.max(Math.ceil(timeframeDuration / ruleIntervalDuration), 1); - interval = advancedOptions.interval; - - const { unit: lookbackUnit, value: lookbackValue } = getTimeTypeValue(advancedOptions.lookback); - duration.add(lookbackValue, lookbackUnit); - - from = `now-${duration.asSeconds()}s`; - } - const showInvocationCountWarning = invocationCount > REASONABLE_INVOCATION_COUNT; - useEffect(() => { if (!rule) { setResponse(emptyPreviewRule); @@ -144,5 +89,5 @@ export const usePreviewRule = ({ }; }, [rule, addError, invocationCount, from, interval, timeframeEnd]); - return { isLoading, showInvocationCountWarning, response, rule, setRule }; + return { isLoading, response, rule, setRule }; }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 3e71ee086d2eed..59f2d0e6fd713a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -17,7 +17,6 @@ import type { List, } from '@kbn/securitysolution-io-ts-list-types'; import type { - ThreatMapping, Threats, ThreatSubtechnique, ThreatTechnique, @@ -27,7 +26,6 @@ import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants'; import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../common/constants'; import { assertUnreachable } from '../../../../../../common/utility_types'; import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions'; -import type { Rule } from '../../../../containers/detection_engine/rules'; import type { AboutStepRule, DefineStepRule, @@ -39,16 +37,10 @@ import type { ActionsStepRuleJson, RuleStepsFormData, RuleStep, - AdvancedPreviewOptions, } from '../types'; import { DataSourceType } from '../types'; -import type { FieldValueQueryBar } from '../../../../components/rules/query_bar'; import type { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; -import { stepDefineDefaultValue } from '../../../../components/rules/step_define_rule'; -import { stepAboutDefaultValue } from '../../../../components/rules/step_about_rule/default_value'; import { stepActionsDefaultValue } from '../../../../components/rules/step_rule_actions'; -import type { FieldValueThreshold } from '../../../../components/rules/threshold_input'; -import type { EqlOptionsSelected } from '../../../../../../common/search_strategy'; export const getTimeTypeValue = (time: string): { unit: Unit; value: number } => { const timeObj: { unit: Unit; value: number } = { @@ -568,89 +560,38 @@ export const formatRule = <T>( aboutStepData: AboutStepRule, scheduleData: ScheduleStepRule, actionsData: ActionsStepRule, - rule?: Rule | null + exceptionsList?: List[] ): T => deepmerge.all([ formatDefineStepData(defineStepData), - formatAboutStepData(aboutStepData, rule?.exceptions_list), + formatAboutStepData(aboutStepData, exceptionsList), formatScheduleStepData(scheduleData), formatActionsStepData(actionsData), ]) as unknown as T; export const formatPreviewRule = ({ - index, - dataViewId, - dataSourceType, - query, - threatIndex, - threatQuery, - ruleType, - threatMapping, - timeFrame, - threshold, - machineLearningJobId, - anomalyThreshold, - eqlOptions, - newTermsFields, - historyWindowSize, - advancedOptions, + defineRuleData, + aboutRuleData, + scheduleRuleData, + exceptionsList, }: { - index: string[]; - dataViewId?: string; - dataSourceType: DataSourceType; - threatIndex: string[]; - query: FieldValueQueryBar; - threatQuery: FieldValueQueryBar; - ruleType: Type; - threatMapping: ThreatMapping; - timeFrame: Unit; - threshold: FieldValueThreshold; - machineLearningJobId: string[]; - anomalyThreshold: number; - eqlOptions: EqlOptionsSelected; - newTermsFields: string[]; - historyWindowSize: string; - advancedOptions?: AdvancedPreviewOptions; + defineRuleData: DefineStepRule; + aboutRuleData: AboutStepRule; + scheduleRuleData: ScheduleStepRule; + exceptionsList?: List[]; }): CreateRulesSchema => { - const defineStepData = { - ...stepDefineDefaultValue, - index, - dataViewId, - dataSourceType, - queryBar: query, - ruleType, - threatIndex, - threatQueryBar: threatQuery, - threatMapping, - threshold, - machineLearningJobId, - anomalyThreshold, - eqlOptions, - newTermsFields, - historyWindowSize, - }; const aboutStepData = { - ...stepAboutDefaultValue, + ...aboutRuleData, name: 'Preview Rule', description: 'Preview Rule', }; - let scheduleStepData = { - from: `now-${timeFrame === 'M' ? '25h' : timeFrame === 'd' ? '65m' : '6m'}`, - interval: `${timeFrame === 'M' ? '1d' : timeFrame === 'd' ? '1h' : '5m'}`, - }; - if (advancedOptions) { - scheduleStepData = { - interval: advancedOptions.interval, - from: advancedOptions.lookback, - }; - } return { ...formatRule<CreateRulesSchema>( - defineStepData, + defineRuleData, aboutStepData, - scheduleStepData, - stepActionsDefaultValue + scheduleRuleData, + stepActionsDefaultValue, + exceptionsList ), - ...(!advancedOptions ? scheduleStepData : {}), }; }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx index 84debb9f9fcc97..37e8c3a0ec9705 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx @@ -6,6 +6,7 @@ */ import { + EuiButton, EuiButtonEmpty, EuiAccordion, EuiHorizontalRule, @@ -17,6 +18,7 @@ import React, { useCallback, useRef, useState, useMemo, useEffect } from 'react' import styled from 'styled-components'; import type { DataViewListItem } from '@kbn/data-views-plugin/common'; +import { isThreatMatchRule } from '../../../../../../common/detection_engine/utils'; import { useCreateRule } from '../../../../containers/detection_engine/rules'; import type { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config'; @@ -42,15 +44,33 @@ import { userHasPermissions, MaxWidthEuiFlexItem, } from '../helpers'; -import type { RuleStepsFormData, RuleStepsFormHooks } from '../types'; +import type { + AboutStepRule, + DefineStepRule, + ScheduleStepRule, + RuleStepsFormData, + RuleStepsFormHooks, + RuleStepsData, +} from '../types'; import { RuleStep } from '../types'; import { formatRule, stepIsValid } from './helpers'; import * as i18n from './translations'; import { SecurityPageName } from '../../../../../app/types'; -import { ruleStepsOrder } from '../utils'; -import { APP_UI_ID } from '../../../../../../common/constants'; -import { useKibana } from '../../../../../common/lib/kibana'; +import { + getStepScheduleDefaultValue, + ruleStepsOrder, + stepAboutDefaultValue, + stepDefineDefaultValue, +} from '../utils'; +import { + APP_UI_ID, + DEFAULT_INDEX_KEY, + DEFAULT_INDICATOR_SOURCE_PATH, + DEFAULT_THREAT_INDEX_KEY, +} from '../../../../../../common/constants'; +import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana'; import { HeaderPage } from '../../../../../common/components/header_page'; +import { PreviewFlyout } from '../preview'; const formHookNoop = async (): Promise<undefined> => undefined; @@ -109,6 +129,10 @@ const CreateRulePageComponent: React.FC = () => { const scheduleRuleRef = useRef<EuiAccordion | null>(null); // @ts-expect-error EUI team to resolve: https://github.com/elastic/eui/issues/5985 const ruleActionsRef = useRef<EuiAccordion | null>(null); + + const [indicesConfig] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); + const [threatIndicesConfig] = useUiSetting$<string[]>(DEFAULT_THREAT_INDEX_KEY); + const formHooks = useRef<RuleStepsFormHooks>({ [RuleStep.defineRule]: formHookNoop, [RuleStep.aboutRule]: formHookNoop, @@ -144,6 +168,44 @@ const CreateRulePageComponent: React.FC = () => { const ruleName = stepsData.current[RuleStep.aboutRule].data?.name; const actionMessageParams = useMemo(() => getActionMessageParams(ruleType), [ruleType]); const [dataViewOptions, setDataViewOptions] = useState<{ [x: string]: DataViewListItem }>({}); + const [isPreviewDisabled, setIsPreviewDisabled] = useState(false); + const [isRulePreviewVisible, setIsRulePreviewVisible] = useState(false); + + const [defineRuleData, setDefineRuleData] = useState<DefineStepRule>({ + ...stepDefineDefaultValue, + index: indicesConfig, + threatIndex: threatIndicesConfig, + }); + const [aboutRuleData, setAboutRuleData] = useState<AboutStepRule>(stepAboutDefaultValue); + const [scheduleRuleData, setScheduleRuleData] = useState<ScheduleStepRule>( + getStepScheduleDefaultValue(defineRuleData.ruleType) + ); + + useEffect(() => { + const isThreatMatchRuleValue = isThreatMatchRule(defineRuleData.ruleType); + if (isThreatMatchRuleValue) { + setAboutRuleData({ + ...stepAboutDefaultValue, + threatIndicatorPath: DEFAULT_INDICATOR_SOURCE_PATH, + }); + } else { + setAboutRuleData(stepAboutDefaultValue); + } + setScheduleRuleData(getStepScheduleDefaultValue(defineRuleData.ruleType)); + }, [defineRuleData.ruleType]); + + const updateCurrentDataState = useCallback( + <K extends keyof RuleStepsData>(data: RuleStepsData[K]) => { + if (activeStep === RuleStep.defineRule) { + setDefineRuleData(data as DefineStepRule); + } else if (activeStep === RuleStep.aboutRule) { + setAboutRuleData(data as AboutStepRule); + } else if (activeStep === RuleStep.scheduleRule) { + setScheduleRuleData(data as ScheduleStepRule); + } + }, + [activeStep] + ); useEffect(() => { const fetchDataViews = async () => { @@ -205,7 +267,8 @@ const CreateRulePageComponent: React.FC = () => { async (step: RuleStep) => { const stepData = await formHooks.current[step](); - if (stepData?.isValid) { + if (stepData?.isValid && stepData.data) { + updateCurrentDataState(stepData.data); setStepData(step, stepData); const nextStep = getNextStep(step); @@ -235,7 +298,7 @@ const CreateRulePageComponent: React.FC = () => { } } }, - [goToStep, setRule] + [goToStep, setRule, updateCurrentDataState] ); const getAccordionType = useCallback( @@ -250,10 +313,6 @@ const CreateRulePageComponent: React.FC = () => { [activeStep] ); - const submitStepDefineRule = useCallback(() => { - submitStep(RuleStep.defineRule); - }, [submitStep]); - const defineRuleButton = ( <AccordionTitle name="1" @@ -326,7 +385,15 @@ const CreateRulePageComponent: React.FC = () => { }} isLoading={isLoading || loading} title={i18n.PAGE_TITLE} - /> + > + <EuiButton + data-test-subj="preview-flyout" + iconType="visBarVerticalStacked" + onClick={() => setIsRulePreviewVisible((isVisible) => !isVisible)} + > + {i18n.RULE_PREVIEW_TITLE} + </EuiButton> + </HeaderPage> <MyEuiPanel zindex={4} hasBorder> <EuiAccordion initialIsOpen={true} @@ -351,16 +418,20 @@ const CreateRulePageComponent: React.FC = () => { <EuiHorizontalRule margin="m" /> <StepDefineRule addPadding={true} - defaultValues={stepsData.current[RuleStep.defineRule].data} + defaultValues={defineRuleData} isReadOnlyView={activeStep !== RuleStep.defineRule} isLoading={isLoading || loading} setForm={setFormHook} - onSubmit={submitStepDefineRule} + onSubmit={() => submitStep(RuleStep.defineRule)} kibanaDataViews={dataViewOptions} descriptionColumns="singleSplit" // We need a key to make this component remount when edit/view mode is toggled // https://github.com/elastic/kibana/pull/132834#discussion_r881705566 key={isShouldRerenderStep(RuleStep.defineRule, activeStep)} + indicesConfig={indicesConfig} + threatIndicesConfig={threatIndicesConfig} + onRuleDataChange={updateCurrentDataState} + onPreviewDisabledStateChange={setIsPreviewDisabled} /> </EuiAccordion> </MyEuiPanel> @@ -389,8 +460,8 @@ const CreateRulePageComponent: React.FC = () => { <EuiHorizontalRule margin="m" /> <StepAboutRule addPadding={true} - defaultValues={stepsData.current[RuleStep.aboutRule].data} - defineRuleData={stepsData.current[RuleStep.defineRule].data} + defaultValues={aboutRuleData} + defineRuleData={defineRuleData} descriptionColumns="singleSplit" isReadOnlyView={activeStep !== RuleStep.aboutRule} isLoading={isLoading || loading} @@ -399,6 +470,7 @@ const CreateRulePageComponent: React.FC = () => { // We need a key to make this component remount when edit/view mode is toggled // https://github.com/elastic/kibana/pull/132834#discussion_r881705566 key={isShouldRerenderStep(RuleStep.aboutRule, activeStep)} + onRuleDataChange={updateCurrentDataState} /> </EuiAccordion> </MyEuiPanel> @@ -425,9 +497,8 @@ const CreateRulePageComponent: React.FC = () => { > <EuiHorizontalRule margin="m" /> <StepScheduleRule - ruleType={ruleType} addPadding={true} - defaultValues={stepsData.current[RuleStep.scheduleRule].data} + defaultValues={scheduleRuleData} descriptionColumns="singleSplit" isReadOnlyView={activeStep !== RuleStep.scheduleRule} isLoading={isLoading || loading} @@ -436,6 +507,7 @@ const CreateRulePageComponent: React.FC = () => { // We need a key to make this component remount when edit/view mode is toggled // https://github.com/elastic/kibana/pull/132834#discussion_r881705566 key={isShouldRerenderStep(RuleStep.scheduleRule, activeStep)} + onRuleDataChange={updateCurrentDataState} /> </EuiAccordion> </MyEuiPanel> @@ -475,6 +547,15 @@ const CreateRulePageComponent: React.FC = () => { /> </EuiAccordion> </MyEuiPanel> + {isRulePreviewVisible && ( + <PreviewFlyout + isDisabled={isPreviewDisabled && activeStep === RuleStep.defineRule} + defineStepData={defineRuleData} + aboutStepData={aboutRuleData} + scheduleStepData={scheduleRuleData} + onClose={() => setIsRulePreviewVisible(false)} + /> + )} </MaxWidthEuiFlexItem> </EuiFlexGroup> </SecuritySolutionPageWrapper> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/translations.ts index f4292f6c663cce..124c3c38052175 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/translations.ts @@ -21,6 +21,28 @@ export const BACK_TO_RULES = i18n.translate( } ); +export const RULE_PREVIEW_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.rulePreviewTitle', + { + defaultMessage: 'Rule preview', + } +); + +export const RULE_PREVIEW_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.rulePreviewDescription', + { + defaultMessage: + 'Rule preview reflects the current configuration of your rule settings and exceptions, click refresh icon to see the updated preview.', + } +); + +export const CANCEL_BUTTON_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } +); + export const EDIT_RULE = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.editRuleButton', { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index cc36e6f75da4b0..7b3c8846577077 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -37,7 +37,7 @@ import { useDeepEqualSelector, useShallowEqualSelector, } from '../../../../../common/hooks/use_selector'; -import { useKibana } from '../../../../../common/lib/kibana'; +import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana'; import { TimelineId } from '../../../../../../common/types/timeline'; import type { UpdateDateRange } from '../../../../../common/components/charts/common'; import { FiltersGlobal } from '../../../../../common/components/filters_global'; @@ -78,7 +78,11 @@ import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_l import { SecurityPageName } from '../../../../../app/types'; import { LinkButton } from '../../../../../common/components/links'; import { useFormatUrl } from '../../../../../common/components/link_to'; -import { APP_UI_ID } from '../../../../../../common/constants'; +import { + APP_UI_ID, + DEFAULT_INDEX_KEY, + DEFAULT_THREAT_INDEX_KEY, +} from '../../../../../../common/constants'; import { useGlobalFullScreen } from '../../../../../common/containers/use_full_screen'; import { Display } from '../../../../../hosts/pages/display'; @@ -301,6 +305,9 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({ const [filterGroup, setFilterGroup] = useState<Status>(FILTER_OPEN); const [dataViewOptions, setDataViewOptions] = useState<{ [x: string]: DataViewListItem }>({}); + const [indicesConfig] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); + const [threatIndicesConfig] = useUiSetting$<string[]>(DEFAULT_THREAT_INDEX_KEY); + useEffect(() => { const fetchDataViews = async () => { const dataViewsRefs = await data.dataViews.getIdsWithTitle(); @@ -767,6 +774,8 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({ isLoading={false} defaultValues={{ dataViewTitle, ...defineRuleData }} kibanaDataViews={dataViewOptions} + indicesConfig={indicesConfig} + threatIndicesConfig={threatIndicesConfig} /> )} </StepPanel> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx index d28a15ac43b392..5f93dbdede83b5 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx @@ -52,16 +52,29 @@ import { MaxWidthEuiFlexItem, } from '../helpers'; import * as ruleI18n from '../translations'; -import type { RuleStepsFormHooks, RuleStepsFormData, RuleStepsData } from '../types'; +import type { + ActionsStepRule, + AboutStepRule, + DefineStepRule, + ScheduleStepRule, + RuleStepsFormHooks, + RuleStepsFormData, + RuleStepsData, +} from '../types'; import { RuleStep } from '../types'; import * as i18n from './translations'; import { SecurityPageName } from '../../../../../app/types'; import { ruleStepsOrder } from '../utils'; -import { useKibana } from '../../../../../common/lib/kibana'; -import { APP_UI_ID } from '../../../../../../common/constants'; +import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana'; +import { + APP_UI_ID, + DEFAULT_INDEX_KEY, + DEFAULT_THREAT_INDEX_KEY, +} from '../../../../../../common/constants'; import { HeaderPage } from '../../../../../common/components/header_page'; import { useStartTransaction } from '../../../../../common/lib/apm/use_start_transaction'; import { SINGLE_RULE_ACTIONS } from '../../../../../common/lib/apm/user_actions'; +import { PreviewFlyout } from '../preview'; const formHookNoop = async (): Promise<undefined> => undefined; @@ -97,10 +110,10 @@ const EditRulePageComponent: FC = () => { [RuleStep.scheduleRule]: { isValid: false, data: undefined }, [RuleStep.ruleActions]: { isValid: false, data: undefined }, }); - const defineStep = stepsData.current[RuleStep.defineRule]; - const aboutStep = stepsData.current[RuleStep.aboutRule]; - const scheduleStep = stepsData.current[RuleStep.scheduleRule]; - const actionsStep = stepsData.current[RuleStep.ruleActions]; + const [defineStep, setDefineStep] = useState(stepsData.current[RuleStep.defineRule]); + const [aboutStep, setAboutStep] = useState(stepsData.current[RuleStep.aboutRule]); + const [scheduleStep, setScheduleStep] = useState(stepsData.current[RuleStep.scheduleRule]); + const [actionsStep, setActionsStep] = useState(stepsData.current[RuleStep.ruleActions]); const [activeStep, setActiveStep] = useState<RuleStep>(RuleStep.defineRule); const invalidSteps = ruleStepsOrder.filter((step) => { const stepData = stepsData.current[step]; @@ -108,6 +121,8 @@ const EditRulePageComponent: FC = () => { }); const [{ isLoading, isSaved }, setRule] = useUpdateRule(); const [dataViewOptions, setDataViewOptions] = useState<{ [x: string]: DataViewListItem }>({}); + const [isPreviewDisabled, setIsPreviewDisabled] = useState(false); + const [isRulePreviewVisible, setIsRulePreviewVisible] = useState(false); useEffect(() => { const fetchDataViews = async () => { @@ -135,11 +150,51 @@ const EditRulePageComponent: FC = () => { ); const setStepData = useCallback( <K extends keyof RuleStepsData>(step: K, data: RuleStepsData[K], isValid: boolean) => { - stepsData.current[step] = { ...stepsData.current[step], data, isValid }; + switch (step) { + case RuleStep.aboutRule: + const aboutData = data as AboutStepRule; + setAboutStep({ ...stepsData.current[step], data: aboutData, isValid }); + return; + case RuleStep.defineRule: + const defineData = data as DefineStepRule; + setDefineStep({ ...stepsData.current[step], data: defineData, isValid }); + return; + case RuleStep.ruleActions: + const actionsData = data as ActionsStepRule; + setActionsStep({ ...stepsData.current[step], data: actionsData, isValid }); + return; + case RuleStep.scheduleRule: + const scheduleData = data as ScheduleStepRule; + setScheduleStep({ ...stepsData.current[step], data: scheduleData, isValid }); + } }, [] ); + const onDataChange = useCallback(async () => { + if (activeStep === RuleStep.defineRule) { + const defineStepData = await formHooks.current[RuleStep.defineRule](); + if (defineStepData?.isValid && defineStepData?.data) { + setDefineStep(defineStepData); + } + } else if (activeStep === RuleStep.aboutRule) { + const aboutStepData = await formHooks.current[RuleStep.aboutRule](); + if (aboutStepData?.isValid && aboutStepData?.data) { + setAboutStep(aboutStepData); + } + } else if (activeStep === RuleStep.scheduleRule) { + const scheduleStepData = await formHooks.current[RuleStep.scheduleRule](); + if (scheduleStepData?.isValid && scheduleStepData?.data) { + setScheduleStep(scheduleStepData); + } + } + }, [activeStep]); + + const onPreviewClose = useCallback(() => setIsRulePreviewVisible(false), []); + + const [indicesConfig] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); + const [threatIndicesConfig] = useUiSetting$<string[]>(DEFAULT_THREAT_INDEX_KEY); + const tabs = useMemo( () => [ { @@ -159,6 +214,10 @@ const EditRulePageComponent: FC = () => { defaultValues={defineStep.data} setForm={setFormHook} kibanaDataViews={dataViewOptions} + indicesConfig={indicesConfig} + threatIndicesConfig={threatIndicesConfig} + onRuleDataChange={onDataChange} + onPreviewDisabledStateChange={setIsPreviewDisabled} /> )} <EuiSpacer /> @@ -183,6 +242,7 @@ const EditRulePageComponent: FC = () => { defaultValues={aboutStep.data} defineRuleData={defineStep.data} setForm={setFormHook} + onRuleDataChange={onDataChange} /> )} <EuiSpacer /> @@ -206,6 +266,7 @@ const EditRulePageComponent: FC = () => { isUpdateView defaultValues={scheduleStep.data} setForm={setFormHook} + onRuleDataChange={onDataChange} /> )} <EuiSpacer /> @@ -243,11 +304,14 @@ const EditRulePageComponent: FC = () => { defineStep.data, isLoading, setFormHook, + dataViewOptions, + indicesConfig, + threatIndicesConfig, + onDataChange, aboutStep.data, scheduleStep.data, actionsStep.data, actionMessageParams, - dataViewOptions, ] ); @@ -276,7 +340,7 @@ const EditRulePageComponent: FC = () => { about.data, schedule.data, actions.data, - rule + rule?.exceptions_list ), ...(ruleId ? { id: ruleId } : {}), ...(rule != null ? { max_signals: rule.max_signals } : {}), @@ -388,7 +452,16 @@ const EditRulePageComponent: FC = () => { }} isLoading={isLoading} title={i18n.PAGE_TITLE} - /> + > + {defineStep.data && aboutStep.data && scheduleStep.data && ( + <EuiButton + iconType="visBarVerticalStacked" + onClick={() => setIsRulePreviewVisible((isVisible) => !isVisible)} + > + {ruleI18n.RULE_PREVIEW_TITLE} + </EuiButton> + )} + </HeaderPage> {invalidSteps.length > 0 && ( <EuiCallOut title={i18n.SORRY_ERRORS} color="danger" iconType="alert"> <FormattedMessage @@ -449,6 +522,16 @@ const EditRulePageComponent: FC = () => { </EuiButton> </EuiFlexItem> </EuiFlexGroup> + {isRulePreviewVisible && defineStep.data && aboutStep.data && scheduleStep.data && ( + <PreviewFlyout + isDisabled={isPreviewDisabled} + defineStepData={defineStep.data} + aboutStepData={aboutStep.data} + scheduleStepData={scheduleStep.data} + exceptionsList={rule?.exceptions_list} + onClose={onPreviewClose} + /> + )} </MaxWidthEuiFlexItem> </EuiFlexGroup> </SecuritySolutionPageWrapper> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/preview/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/preview/index.tsx new file mode 100644 index 00000000000000..039ab407755689 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/preview/index.tsx @@ -0,0 +1,80 @@ +/* + * 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 { + EuiButton, + EuiSpacer, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import type { List } from '@kbn/securitysolution-io-ts-list-types'; +import { RulePreview } from '../../../../components/rules/rule_preview'; +import type { AboutStepRule, DefineStepRule, ScheduleStepRule } from '../types'; + +import * as i18n from './translations'; + +const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` + overflow-y: hidden; + flex: 1; + + .euiFlyoutBody__overflow { + mask-image: none; + } +`; + +interface PreviewFlyoutProps { + isDisabled: boolean; + defineStepData: DefineStepRule; + aboutStepData: AboutStepRule; + scheduleStepData: ScheduleStepRule; + exceptionsList?: List[]; + onClose: () => void; +} + +const PreviewFlyoutComponent: React.FC<PreviewFlyoutProps> = ({ + isDisabled, + defineStepData, + aboutStepData, + scheduleStepData, + exceptionsList, + onClose, +}) => { + return ( + <EuiFlyout type="push" size="550px" ownFocus={false} onClose={onClose}> + <EuiFlyoutHeader hasBorder> + <EuiTitle size="m"> + <h2>{i18n.RULE_PREVIEW_TITLE}</h2> + </EuiTitle> + <EuiSpacer size="s" /> + <EuiText color="subdued"> + <p>{i18n.RULE_PREVIEW_DESCRIPTION}</p> + </EuiText> + </EuiFlyoutHeader> + <StyledEuiFlyoutBody> + <RulePreview + isDisabled={isDisabled} + defineRuleData={defineStepData} + aboutRuleData={aboutStepData} + scheduleRuleData={scheduleStepData} + exceptionsList={exceptionsList} + /> + </StyledEuiFlyoutBody> + <EuiFlyoutFooter> + <EuiButton onClick={onClose}>{i18n.CANCEL_BUTTON_LABEL}</EuiButton> + </EuiFlyoutFooter> + </EuiFlyout> + ); +}; + +export const PreviewFlyout = React.memo(PreviewFlyoutComponent); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/preview/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/preview/translations.ts new file mode 100644 index 00000000000000..9e3e3af9fd6a16 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/preview/translations.ts @@ -0,0 +1,30 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const RULE_PREVIEW_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.rulePreviewTitle', + { + defaultMessage: 'Rule preview', + } +); + +export const RULE_PREVIEW_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.rulePreviewDescription', + { + defaultMessage: + 'Rule preview reflects the current configuration of your rule settings and exceptions, click refresh icon to see the updated preview.', + } +); + +export const CANCEL_BUTTON_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index 676356130bada8..6994d028e0e4dd 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -1090,3 +1090,25 @@ export const NEW_TERMS_TOUR_CONTENT = i18n.translate( defaultMessage: '"New Terms" rules alert on values that have not previously been seen', } ); + +export const RULE_PREVIEW_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.rulePreviewTitle', + { + defaultMessage: 'Rule preview', + } +); + +export const RULE_PREVIEW_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.rulePreviewDescription', + { + defaultMessage: + 'Rule preview reflects the current configuration of your rule settings and exceptions, click refresh icon to see the updated preview.', + } +); + +export const CANCEL_BUTTON_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index 93910509193e2b..d262cd44dadfcf 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -16,10 +16,9 @@ import type { SeverityMapping, Severity, } from '@kbn/securitysolution-io-ts-alerting-types'; -import type { Filter } from '@kbn/es-query'; +import type { DataViewBase, Filter } from '@kbn/es-query'; import type { RuleAction } from '@kbn/alerting-plugin/common'; import type { DataViewListItem } from '@kbn/data-views-plugin/common'; -import type { Unit } from '@kbn/datemath'; import type { RuleAlertAction } from '../../../../../common/detection_engine/types'; import type { FieldValueQueryBar } from '../../../components/rules/query_bar'; @@ -146,6 +145,7 @@ export enum DataSourceType { export interface DefineStepRule { anomalyThreshold: number; index: string[]; + indexPattern?: DataViewBase; machineLearningJobId: string[]; queryBar: FieldValueQueryBar; dataViewId?: string; @@ -243,17 +243,7 @@ export interface ActionsStepRuleJson { meta?: unknown; } -export interface QuickQueryPreviewOptions { - timeframe: Unit; - timeframeEnd: moment.Moment; -} - -export interface AdvancedPreviewForm { - interval: string; - lookback: string; -} - -export interface AdvancedPreviewOptions { +export interface TimeframePreviewOptions { timeframeStart: moment.Moment; timeframeEnd: moment.Moment; interval: string; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts index b03ad5b8869877..806c236fd63cce 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts @@ -6,6 +6,9 @@ */ import type { ChromeBreadcrumb } from '@kbn/core/public'; +import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; +import { isThreatMatchRule } from '../../../../../common/detection_engine/utils'; +import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations'; import { getRuleDetailsTabUrl, getRuleDetailsUrl, @@ -13,11 +16,12 @@ import { import * as i18nRules from './translations'; import type { RouteSpyState } from '../../../../common/utils/route/types'; import { SecurityPageName } from '../../../../app/types'; -import { RULES_PATH } from '../../../../../common/constants'; -import type { RuleStepsOrder } from './types'; -import { RuleStep } from './types'; +import { DEFAULT_THREAT_MATCH_QUERY, RULES_PATH } from '../../../../../common/constants'; +import type { AboutStepRule, DefineStepRule, RuleStepsOrder, ScheduleStepRule } from './types'; +import { DataSourceType, RuleStep } from './types'; import type { GetSecuritySolutionUrl } from '../../../../common/components/link_to'; import { RuleDetailTabs, RULE_DETAILS_TAB_NAME } from './details'; +import { fillEmptySeverityMappings } from './helpers'; export const ruleStepsOrder: RuleStepsOrder = [ RuleStep.defineRule, @@ -90,3 +94,97 @@ export const getTrailingBreadcrumbs = ( return breadcrumb; }; + +export const threatDefault = [ + { + framework: 'MITRE ATT&CK', + tactic: { id: 'none', name: 'none', reference: 'none' }, + technique: [], + }, +]; + +export const stepDefineDefaultValue: DefineStepRule = { + anomalyThreshold: 50, + index: [], + indexPattern: { fields: [], title: '' }, + machineLearningJobId: [], + ruleType: 'query', + threatIndex: [], + queryBar: { + query: { query: '', language: 'kuery' }, + filters: [], + saved_id: null, + }, + threatQueryBar: { + query: { query: DEFAULT_THREAT_MATCH_QUERY, language: 'kuery' }, + filters: [], + saved_id: null, + }, + requiredFields: [], + relatedIntegrations: [], + threatMapping: [], + threshold: { + field: [], + value: '200', + cardinality: { + field: [], + value: '', + }, + }, + timeline: { + id: null, + title: DEFAULT_TIMELINE_TITLE, + }, + eqlOptions: {}, + dataSourceType: DataSourceType.IndexPatterns, + newTermsFields: [], + historyWindowSize: '7d', +}; + +export const stepAboutDefaultValue: AboutStepRule = { + author: [], + name: '', + description: '', + isAssociatedToEndpointList: false, + isBuildingBlock: false, + severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false }, + riskScore: { value: 21, mapping: [], isMappingChecked: false }, + references: [''], + falsePositives: [''], + license: '', + ruleNameOverride: '', + tags: [], + timestampOverride: '', + threat: threatDefault, + note: '', + threatIndicatorPath: undefined, + timestampOverrideFallbackDisabled: undefined, +}; + +const DEFAULT_INTERVAL = '5m'; +const DEFAULT_FROM = '1m'; +const THREAT_MATCH_INTERVAL = '1h'; +const THREAT_MATCH_FROM = '5m'; + +export const getStepScheduleDefaultValue = (ruleType: Type | undefined): ScheduleStepRule => { + return { + interval: isThreatMatchRule(ruleType) ? THREAT_MATCH_INTERVAL : DEFAULT_INTERVAL, + from: isThreatMatchRule(ruleType) ? THREAT_MATCH_FROM : DEFAULT_FROM, + }; +}; + +/** + * This default query will be used for threat query/indicator matches + * as the default when the user swaps to using it by changing their + * rule type from any rule type to the "threatMatchRule" type. Only + * difference is that "*:*" is used instead of '' for its query. + */ +const threatQueryBarDefaultValue: DefineStepRule['queryBar'] = { + ...stepDefineDefaultValue.queryBar, + query: { ...stepDefineDefaultValue.queryBar.query, query: '*:*' }, +}; + +export const defaultCustomQuery = { + forNormalRules: stepDefineDefaultValue.queryBar, + forThreatMatchRules: threatQueryBarDefaultValue, +}; diff --git a/x-pack/plugins/security_solution/public/hosts/components/host_risk_information/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/host_risk_information/index.tsx index 5d95d6e7f9446b..ea1ecf8c9d6524 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/host_risk_information/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/host_risk_information/index.tsx @@ -27,12 +27,11 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; -import { RISKY_HOSTS_DOC_LINK } from '../../../overview/components/overview_risky_host_links/risky_hosts_disabled_module'; - import * as i18n from './translations'; import { useOnOpenCloseHandler } from '../../../helper_hooks'; import { RiskScore } from '../../../common/components/severity/common'; import { RiskSeverity } from '../../../../common/search_strategy'; +import { RISKY_HOSTS_DOC_LINK } from '../../../../common/constants'; const tableColumns: Array<EuiBasicTableColumn<TableItem>> = [ { @@ -129,9 +128,9 @@ const HostRiskInformationFlyout = ({ handleOnClose }: { handleOnClose: () => voi <EuiSpacer size="l" /> <FormattedMessage id="xpack.securitySolution.hosts.hostRiskInformation.learnMore" - defaultMessage="You can learn more about host risk {hostsRiskScoreDocumentationLink}" + defaultMessage="You can learn more about host risk {HostRiskScoreDocumentationLink}" values={{ - hostsRiskScoreDocumentationLink: ( + HostRiskScoreDocumentationLink: ( <EuiLink href={RISKY_HOSTS_DOC_LINK} target="_blank"> <FormattedMessage id="xpack.securitySolution.hosts.hostRiskInformation.link" diff --git a/x-pack/plugins/security_solution/public/hosts/components/host_risk_score_table/columns.tsx b/x-pack/plugins/security_solution/public/hosts/components/host_risk_score_table/columns.tsx index 16d0a0d848acba..b7de3eba5ebe64 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/host_risk_score_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/host_risk_score_table/columns.tsx @@ -21,6 +21,7 @@ import type { HostRiskScoreColumns } from '.'; import * as i18n from './translations'; import { HostsTableType } from '../../store/model'; import type { RiskSeverity } from '../../../../common/search_strategy'; +import { RiskScoreFields } from '../../../../common/search_strategy'; import { RiskScore } from '../../../common/components/severity/common'; export const getHostRiskScoreColumns = ({ @@ -67,7 +68,7 @@ export const getHostRiskScoreColumns = ({ }, }, { - field: 'risk_stats.risk_score', + field: RiskScoreFields.hostRiskScore, name: i18n.HOST_RISK_SCORE, truncateText: true, mobileOptions: { show: true }, @@ -84,7 +85,7 @@ export const getHostRiskScoreColumns = ({ }, }, { - field: 'risk', + field: RiskScoreFields.hostRisk, name: ( <EuiToolTip content={i18n.HOST_RISK_TOOLTIP}> <> diff --git a/x-pack/plugins/security_solution/public/hosts/components/host_risk_score_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/host_risk_score_table/index.tsx index 38daf27402c545..9a2138786b3a87 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/host_risk_score_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/host_risk_score_table/index.tsx @@ -16,7 +16,7 @@ import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { hostsActions, hostsModel, hostsSelectors } from '../../store'; import { getHostRiskScoreColumns } from './columns'; import type { - HostsRiskScore, + HostRiskScore, RiskScoreItem, RiskScoreSortField, RiskSeverity, @@ -50,7 +50,7 @@ const IconWrapper = styled.span` const tableType = hostsModel.HostsTableType.risk; interface HostRiskScoreTableProps { - data: HostsRiskScore[]; + data: HostRiskScore[]; id: string; isInspect: boolean; loading: boolean; @@ -63,8 +63,8 @@ interface HostRiskScoreTableProps { export type HostRiskScoreColumns = [ Columns<RiskScoreItem[RiskScoreFields.hostName]>, - Columns<RiskScoreItem[RiskScoreFields.riskScore]>, - Columns<RiskScoreItem[RiskScoreFields.risk]> + Columns<RiskScoreItem[RiskScoreFields.hostRiskScore]>, + Columns<RiskScoreItem[RiskScoreFields.hostRisk]> ]; const HostRiskScoreTableComponent: React.FC<HostRiskScoreTableProps> = ({ diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx index 7c73cb4f245086..f7c9352f3a9512 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx @@ -12,9 +12,9 @@ import { HostsKpiHosts } from './hosts'; import { HostsKpiUniqueIps } from './unique_ips'; import type { HostsKpiProps } from './types'; import { CallOutSwitcher } from '../../../common/components/callouts'; -import { RISKY_HOSTS_DOC_LINK } from '../../../overview/components/overview_risky_host_links/risky_hosts_disabled_module'; import * as i18n from './translations'; import { useHostRiskScore } from '../../../risk_score/containers'; +import { RISKY_HOSTS_DOC_LINK } from '../../../../common/constants'; export const HostsKpiComponent = React.memo<HostsKpiProps>( ({ filterQuery, from, indexNames, to, setQuery, skip, updateDateRange }) => { diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index b8e62a9bed97c1..9d6687a771f363 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -28,7 +28,7 @@ import { SecuritySolutionPageWrapper } from '../../common/components/page_wrappe import { useGlobalFullScreen } from '../../common/containers/use_full_screen'; import { useGlobalTime } from '../../common/containers/use_global_time'; import { TimelineId } from '../../../common/types/timeline'; -import { LastEventIndexKey } from '../../../common/search_strategy'; +import { LastEventIndexKey, RiskScoreEntity } from '../../../common/search_strategy'; import { useKibana } from '../../common/lib/kibana'; import { convertToBuildEsQuery } from '../../common/lib/keury'; import type { State } from '../../common/store'; @@ -58,7 +58,6 @@ import { ID } from '../containers/hosts'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { LandingPageComponent } from '../../common/components/landing_page'; -import { Loader } from '../../common/components/loader'; import { hostNameExistsFilter } from '../../common/components/visualization_actions/utils'; /** @@ -103,7 +102,7 @@ const HostsComponent = () => { } if (tabName === HostsTableType.risk) { - const severityFilter = generateSeverityFilter(severitySelection); + const severityFilter = generateSeverityFilter(severitySelection, RiskScoreEntity.host); return [...severityFilter, ...hostNameExistsFilter, ...filters]; } @@ -125,7 +124,7 @@ const HostsComponent = () => { }, [dispatch] ); - const { indicesExist, indexPattern, selectedPatterns, loading } = useSourcererDataView(); + const { indicesExist, indexPattern, selectedPatterns } = useSourcererDataView(); const [filterQuery, kqlError] = useMemo( () => convertToBuildEsQuery({ @@ -175,10 +174,6 @@ const HostsComponent = () => { [containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable] ); - if (loading) { - return <Loader data-test-subj="loadingPanelExploreHosts" overlay size="xl" />; - } - return ( <> {indicesExist ? ( diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.tsx index 67c9bb761be94d..33565cd9a34e11 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.tsx @@ -9,6 +9,7 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; +import { last } from 'lodash/fp'; import type { HostsComponentsQueryProps } from './types'; import * as i18n from '../translations'; import { HostRiskInformationButtonEmpty } from '../../components/host_risk_information'; @@ -86,7 +87,7 @@ const HostRiskTabBodyComponent: React.FC< [setOverTimeToggleStatus] ); - const rules = data && data.length > 0 ? data[data.length - 1].risk_stats.rule_risks : []; + const lastHostRiskItem = last(data); return ( <> @@ -110,7 +111,7 @@ const HostRiskTabBodyComponent: React.FC< queryId={QUERY_ID} toggleStatus={contributorsToggleStatus} toggleQuery={toggleContributorsQuery} - rules={rules} + rules={lastHostRiskItem ? lastHostRiskItem.host.risk.rule_risks : []} /> </EuiFlexItem> </EuiFlexGroup> diff --git a/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts b/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts index fd4830f93159fc..daaa2e54ca300e 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts @@ -40,7 +40,7 @@ export const mockHostsState: HostsModel = { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, sort: { - field: RiskScoreFields.riskScore, + field: RiskScoreFields.hostRiskScore, direction: Direction.desc, }, severitySelection: [], @@ -79,7 +79,7 @@ export const mockHostsState: HostsModel = { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, sort: { - field: RiskScoreFields.riskScore, + field: RiskScoreFields.hostRiskScore, direction: Direction.desc, }, severitySelection: [], @@ -124,7 +124,7 @@ describe('Hosts redux store', () => { severitySelection: [], sort: { direction: 'desc', - field: 'risk_stats.risk_score', + field: RiskScoreFields.hostRiskScore, }, }, [HostsTableType.sessions]: { @@ -164,7 +164,7 @@ describe('Hosts redux store', () => { severitySelection: [], sort: { direction: 'desc', - field: 'risk_stats.risk_score', + field: RiskScoreFields.hostRiskScore, }, }, [HostsTableType.sessions]: { diff --git a/x-pack/plugins/security_solution/public/hosts/store/helpers.ts b/x-pack/plugins/security_solution/public/hosts/store/helpers.ts index 6093a2c72a3a9c..eaf1bb5d7c5aa8 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/helpers.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/helpers.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { RiskScoreEntity, RiskScoreFields } from '../../../common/search_strategy'; import type { RiskSeverity } from '../../../common/search_strategy'; import { DEFAULT_TABLE_ACTIVE_PAGE } from '../../common/store/constants'; @@ -60,7 +61,10 @@ export const setHostsQueriesActivePageToZero = (state: HostsModel, type: HostsTy throw new Error(`HostsType ${type} is unknown`); }; -export const generateSeverityFilter = (severitySelection: RiskSeverity[]) => +export const generateSeverityFilter = ( + severitySelection: RiskSeverity[], + entity: RiskScoreEntity +) => severitySelection.length > 0 ? [ { @@ -68,7 +72,9 @@ export const generateSeverityFilter = (severitySelection: RiskSeverity[]) => bool: { should: severitySelection.map((query) => ({ match_phrase: { - 'risk.keyword': { + [entity === RiskScoreEntity.user + ? RiskScoreFields.userRisk + : RiskScoreFields.hostRisk]: { query, }, }, diff --git a/x-pack/plugins/security_solution/public/hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/hosts/store/reducer.ts index 15f4d979a7267f..f549b07b3850b2 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/reducer.ts @@ -59,7 +59,7 @@ export const initialHostsState: HostsState = { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, sort: { - field: RiskScoreFields.riskScore, + field: RiskScoreFields.hostRiskScore, direction: Direction.desc, }, severitySelection: [], @@ -98,7 +98,7 @@ export const initialHostsState: HostsState = { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, sort: { - field: RiskScoreFields.riskScore, + field: RiskScoreFields.hostRiskScore, direction: Direction.desc, }, severitySelection: [], diff --git a/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts b/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts index bfeccafd2e977f..12dfa0f28208a3 100644 --- a/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts +++ b/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts @@ -9,7 +9,7 @@ import type { ChromeBreadcrumb } from '@kbn/core/public'; import { AdministrationSubTab } from '../types'; import { ENDPOINTS_TAB, EVENT_FILTERS_TAB, POLICIES_TAB, TRUSTED_APPS_TAB } from './translations'; import type { AdministrationRouteSpyState } from '../../common/utils/route/types'; -import { HOST_ISOLATION_EXCEPTIONS, BLOCKLIST, RESPONSE_ACTIONS } from '../../app/translations'; +import { HOST_ISOLATION_EXCEPTIONS, BLOCKLIST, ACTION_HISTORY } from '../../app/translations'; const TabNameMappedToI18nKey: Record<AdministrationSubTab, string> = { [AdministrationSubTab.endpoints]: ENDPOINTS_TAB, @@ -18,7 +18,7 @@ const TabNameMappedToI18nKey: Record<AdministrationSubTab, string> = { [AdministrationSubTab.eventFilters]: EVENT_FILTERS_TAB, [AdministrationSubTab.hostIsolationExceptions]: HOST_ISOLATION_EXCEPTIONS, [AdministrationSubTab.blocklist]: BLOCKLIST, - [AdministrationSubTab.responseActions]: RESPONSE_ACTIONS, + [AdministrationSubTab.actionHistory]: ACTION_HISTORY, }; export function getTrailingBreadcrumbs(params: AdministrationRouteSpyState): ChromeBreadcrumb[] { diff --git a/x-pack/plugins/security_solution/public/management/common/constants.ts b/x-pack/plugins/security_solution/public/management/common/constants.ts index a46a9d8a9397f3..afad5b78e9f4e6 100644 --- a/x-pack/plugins/security_solution/public/management/common/constants.ts +++ b/x-pack/plugins/security_solution/public/management/common/constants.ts @@ -23,7 +23,7 @@ export const MANAGEMENT_ROUTING_TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/:tabName export const MANAGEMENT_ROUTING_EVENT_FILTERS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.eventFilters})`; export const MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.hostIsolationExceptions})`; export const MANAGEMENT_ROUTING_BLOCKLIST_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.blocklist})`; -export const MANAGEMENT_ROUTING_RESPONSE_ACTIONS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.responseActions})`; +export const MANAGEMENT_ROUTING_ACTION_HISTORY_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.actionHistory})`; // --[ STORE ]--------------------------------------------------------------------------- /** The SIEM global store namespace where the management state will be mounted */ diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.test.tsx index df23ac288806b2..9160732e32b3eb 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.test.tsx @@ -16,7 +16,7 @@ import { getDeferred } from '../mocks'; jest.mock('../../../common/components/user_privileges'); -// FLAKY: https://github.com/elastic/kibana/issues/129837 +// FLAKY: https://github.com/elastic/kibana/issues/140620 describe.skip('When using the ArtifactListPage component', () => { let render: ( props?: Partial<ArtifactListPageProps> @@ -156,7 +156,8 @@ describe.skip('When using the ArtifactListPage component', () => { expect(getByTestId('testPage-flyout')).toBeTruthy(); }); - it('should display the Delete modal when delete action is clicked', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/129837 + it.skip('should display the Delete modal when delete action is clicked', async () => { const { getByTestId } = await renderWithListData(); await clickCardAction('delete'); @@ -227,7 +228,8 @@ describe.skip('When using the ArtifactListPage component', () => { }); }); - it('should persist policy filter to the URL params', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/129837 + it.skip('should persist policy filter to the URL params', async () => { const policyId = mockedApi.responseProvider.endpointPackagePolicyList().items[0].id; const firstPolicyTestId = `policiesSelector-popover-items-${policyId}`; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_delete_modal.test.ts b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_delete_modal.test.ts index 497baa999cf2e5..d0fb3e3c59dfa5 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_delete_modal.test.ts +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_delete_modal.test.ts @@ -77,12 +77,14 @@ describe('When displaying the Delete artifact modal in the Artifact List Page', 10000 ); - it('should show Cancel and Delete buttons enabled', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/139527 + it.skip('should show Cancel and Delete buttons enabled', async () => { expect(cancelButton).toBeEnabled(); expect(submitButton).toBeEnabled(); }); - it('should close modal if Cancel/Close buttons are clicked', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/139528 + it.skip('should close modal if Cancel/Close buttons are clicked', async () => { userEvent.click(cancelButton); expect(renderResult.queryByTestId('testPage-deleteModal')).toBeNull(); diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/command_input/command_input.test.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/command_input/command_input.test.tsx index 04a57ad6ec9f67..25d7409036d8c8 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/command_input/command_input.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/command_input/command_input.test.tsx @@ -42,6 +42,10 @@ describe('When entering data into the Console input', () => { return renderResult.getByTestId('test-footer').textContent; }; + const typeKeyboardKey = (key: string) => { + enterCommand(key, { inputOnly: true, useKeyboard: true }); + }; + beforeEach(() => { const testSetup = getConsoleTestSetup(); @@ -59,6 +63,20 @@ describe('When entering data into the Console input', () => { expect(getUserInputText()).toEqual('cm'); }); + it('should repeat letters if the user holds letter key down on the keyboard', () => { + render(); + enterCommand('{a>5/}', { inputOnly: true, useKeyboard: true }); + expect(getUserInputText()).toEqual('aaaaa'); + }); + + it('should not display command key names in the input, when command keys are used', () => { + render(); + enterCommand('{Meta>}', { inputOnly: true, useKeyboard: true }); + expect(getUserInputText()).toEqual(''); + enterCommand('{Shift>}A{/Shift}', { inputOnly: true, useKeyboard: true }); + expect(getUserInputText()).toEqual('A'); + }); + it('should display placeholder text when input area is blank', () => { render(); @@ -110,6 +128,23 @@ describe('When entering data into the Console input', () => { expect(arrowButton).toBeDisabled(); }); + it('should show the arrow button as disabled if input has only whitespace entered and it is left to the cursor', () => { + render(); + enterCommand(' ', { inputOnly: true }); + + const arrowButton = renderResult.getByTestId('test-inputTextSubmitButton'); + expect(arrowButton).toBeDisabled(); + }); + + it('should show the arrow button as disabled if input has only whitespace entered and it is right to the cursor', () => { + render(); + enterCommand(' ', { inputOnly: true }); + typeKeyboardKey('{ArrowLeft}'); + + const arrowButton = renderResult.getByTestId('test-inputTextSubmitButton'); + expect(arrowButton).toBeDisabled(); + }); + it('should execute correct command if arrow button is clicked', () => { render(); enterCommand('isolate', { inputOnly: true }); @@ -186,10 +221,6 @@ describe('When entering data into the Console input', () => { return renderResult.getByTestId('test-cmdInput-rightOfCursor').textContent; }; - const typeKeyboardKey = (key: string) => { - enterCommand(key, { inputOnly: true, useKeyboard: true }); - }; - beforeEach(() => { render(); enterCommand('isolate', { inputOnly: true }); @@ -201,6 +232,11 @@ describe('When entering data into the Console input', () => { expect(getRightOfCursorText()).toEqual(''); }); + it('should clear the input if the user holds down the delete/backspace key', () => { + typeKeyboardKey('{backspace>7/}'); + expect(getUserInputText()).toEqual(''); + }); + it('should move cursor to the left', () => { typeKeyboardKey('{ArrowLeft}'); typeKeyboardKey('{ArrowLeft}'); diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/command_input/command_input.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/command_input/command_input.tsx index 92f5c9ff33b0eb..0829c1af193edb 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/command_input/command_input.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/command_input/command_input.tsx @@ -82,7 +82,7 @@ export interface CommandInputProps extends CommonProps { export const CommandInput = memo<CommandInputProps>(({ prompt = '', focusRef, ...commonProps }) => { useInputHints(); const dispatch = useConsoleStateDispatch(); - const { rightOfCursor, textEntered } = useWithInputTextEntered(); + const { rightOfCursor, textEntered, fullTextEntered } = useWithInputTextEntered(); const visibleState = useWithInputVisibleState(); const [isKeyInputBeingCaptured, setIsKeyInputBeingCaptured] = useState(false); const getTestId = useTestIdGenerator(useDataTestSubj()); @@ -117,10 +117,7 @@ export const CommandInput = memo<CommandInputProps>(({ prompt = '', focusRef, .. }); }, [isKeyInputBeingCaptured, visibleState]); - const disableArrowButton = useMemo( - () => textEntered.length === 0 && rightOfCursor.text.length === 0, - [rightOfCursor.text.length, textEntered.length] - ); + const disableArrowButton = useMemo(() => fullTextEntered.trim().length === 0, [fullTextEntered]); const handleSubmitButton = useCallback<MouseEventHandler>( (ev) => { diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/command_input/key_capture.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/command_input/key_capture.tsx index a88cffed733a6f..b5c999427e1d41 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/command_input/key_capture.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/command_input/key_capture.tsx @@ -5,7 +5,12 @@ * 2.0. */ -import type { FormEventHandler, KeyboardEventHandler, MutableRefObject } from 'react'; +import type { + ClipboardEventHandler, + FormEventHandler, + KeyboardEventHandler, + MutableRefObject, +} from 'react'; import React, { memo, useCallback, useMemo, useRef, useState } from 'react'; import { pick } from 'lodash'; import styled from 'styled-components'; @@ -65,12 +70,11 @@ export const KeyCapture = memo<KeyCaptureProps>(({ onCapture, focusRef, onStateC // We don't need the actual value that was last input in this component, because // `setLastInput()` is used with a function that returns the typed character. // This state is used like this: - // 1. user presses a keyboard key - // 2. `input` event is triggered - we store the letter typed - // 3. the next event to be triggered (after `input`) that we listen for is `keyup`, - // and when that is triggered, we take the input letter (already stored) and - // call `onCapture()` with it and then set the lastInput state back to an empty string - const [, setLastInput] = useState(''); + // 1. User presses a keyboard key down + // 2. We store the key that was pressed + // 3. When the 'keyup' event is triggered, we call `onCapture()` + // with all of the character that were entered + // 4. We set the last input back to an empty string const getTestId = useTestIdGenerator(useDataTestSubj()); const inputRef = useRef<HTMLInputElement | null>(null); const blurInputRef = useRef<HTMLInputElement | null>(null); @@ -96,15 +100,36 @@ export const KeyCapture = memo<KeyCaptureProps>(({ onCapture, focusRef, onStateC [onStateChange] ); - const handleOnKeyUp = useCallback<KeyboardEventHandler<HTMLInputElement>>( + const handleInputOnPaste = useCallback<ClipboardEventHandler>( (ev) => { - // There is a condition (still not clear how it is actually happening) where the `Enter` key - // event from the EuiSelectable component gets captured here by the Input. Its likely due to - // the sequence of events between keyup, focus and the Focus trap component having the - // `returnFocus` on by default. - // To avoid having that key Event from actually being processed, we check for this custom - // property on the event and skip processing it if we find it. This property is currently - // set by the CommandInputHistory (using EuiSelectable). + const value = ev.clipboardData.getData('text'); + ev.stopPropagation(); + + // hard-coded for use in onCapture and future keyboard functions + const metaKey = { + altKey: false, + ctrlKey: false, + key: 'Meta', + keyCode: 91, + metaKey: true, + repeat: false, + shiftKey: false, + }; + + onCapture({ + value, + eventDetails: metaKey, + }); + }, + [onCapture] + ); + + // 1. Determine if the key press is one that we need to store ex) letters, digits, values that we see + // 2. If the user clicks a key we don't need to store as text, but we need to do logic with ex) backspace, delete, l/r arrows, we must call onCapture + const handleOnKeyDown = useCallback<KeyboardEventHandler>( + (ev) => { + // checking to ensure that the key is not a control character + const newValue = /^[\w\d]{2}/.test(ev.key) ? '' : ev.key; // @ts-expect-error if (!isCapturing || ev._CONSOLE_IGNORE_KEY) { @@ -119,6 +144,11 @@ export const KeyCapture = memo<KeyCaptureProps>(({ onCapture, focusRef, onStateC ev.stopPropagation(); + // allows for clipboard events to be captured via onPaste event handler + if (ev.metaKey || ev.ctrlKey) { + return; + } + const eventDetails = pick(ev, [ 'key', 'altKey', @@ -129,26 +159,14 @@ export const KeyCapture = memo<KeyCaptureProps>(({ onCapture, focusRef, onStateC 'shiftKey', ]); - setLastInput((value) => { - onCapture({ - value, - eventDetails, - }); - - return ''; + onCapture({ + value: newValue, + eventDetails, }); }, [isCapturing, onCapture] ); - const handleOnInput = useCallback<FormEventHandler<HTMLInputElement>>((ev) => { - const newValue = ev.currentTarget.value; - - setLastInput((prevState) => { - return `${prevState || ''}${newValue}`; - }); - }, []); - const keyCaptureFocusMethods = useMemo<KeyCaptureFocusInterface>(() => { return { focus: (force: boolean = false) => { @@ -183,10 +201,10 @@ export const KeyCapture = memo<KeyCaptureProps>(({ onCapture, focusRef, onStateC spellCheck="false" value="" tabIndex={-1} - onInput={handleOnInput} - onKeyUp={handleOnKeyUp} + onKeyDown={handleOnKeyDown} onBlur={handleInputOnBlur} onFocus={handleInputOnFocus} + onPaste={handleInputOnPaste} onChange={NOOP} // this just silences Jest output warnings ref={inputRef} /> diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/get_processes_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/get_processes_action.test.tsx index bb065a9392d435..29b6fd04465772 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/get_processes_action.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/get_processes_action.test.tsx @@ -194,7 +194,8 @@ describe('When using processes action from response actions console', () => { }); }); - it('should display completion output if done (no additional API calls)', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/139707 + it.skip('should display completion output if done (no additional API calls)', async () => { await render(); expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/kill_process_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/kill_process_action.test.tsx index 167c3feb554a75..827a4d61917541 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/kill_process_action.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/kill_process_action.test.tsx @@ -283,7 +283,8 @@ describe('When using the kill-process action from response actions console', () }); }); - it('should display completion output if done (no additional API calls)', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/139962 + it.skip('should display completion output if done (no additional API calls)', async () => { await render(); expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/release_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/release_action.test.tsx index e729185b220cc6..19e3be94469eb5 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/release_action.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/release_action.test.tsx @@ -20,8 +20,7 @@ import { getDeferred } from '../mocks'; import type { ResponderCapabilities } from '../../../../common/endpoint/constants'; import { RESPONDER_CAPABILITIES } from '../../../../common/endpoint/constants'; -// FLAKY: https://github.com/elastic/kibana/issues/139641 -describe.skip('When using the release action from response actions console', () => { +describe('When using the release action from response actions console', () => { let render: ( capabilities?: ResponderCapabilities[] ) => Promise<ReturnType<AppContextTestRender['render']>>; @@ -205,7 +204,8 @@ describe.skip('When using the release action from response actions console', () }); }); - it('should display completion output if done (no additional API calls)', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/139641 + it.skip('should display completion output if done (no additional API calls)', async () => { await render(); expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/suspend_process_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/suspend_process_action.test.tsx index 4d12af721a02f5..9446fb5dcba6a7 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/suspend_process_action.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/suspend_process_action.test.tsx @@ -274,7 +274,8 @@ describe('When using the suspend-process action from response actions console', }); }); - it('should display completion output if done (no additional API calls)', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/140119 + it.skip('should display completion output if done (no additional API calls)', async () => { await render(); expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_date_range_picker.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_date_range_picker.tsx index 015fd3a501621a..c1c18c4c3cd3c9 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_date_range_picker.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_date_range_picker.tsx @@ -7,7 +7,7 @@ import React, { memo, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker } from '@elastic/eui'; -import type { IDataPluginServices } from '@kbn/data-plugin/public'; +import type { IUnifiedSearchPluginServices } from '@kbn/unified-search-plugin/public'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; import type { EuiSuperDatePickerRecentRange } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; @@ -16,8 +16,8 @@ import type { OnRefreshChangeProps, } from '@elastic/eui/src/components/date_picker/types'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; - import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; +import { useActionHistoryUrlParams } from './use_action_history_url_params'; export interface DateRangePickerValues { autoRefreshOptions: { @@ -37,18 +37,21 @@ export const ActionLogDateRangePicker = memo( ({ dateRangePickerState, isDataLoading, + isFlyout, onRefresh, onRefreshChange, onTimeChange, }: { dateRangePickerState: DateRangePickerValues; isDataLoading: boolean; + isFlyout: boolean; onRefresh: () => void; onRefreshChange: (evt: OnRefreshChangeProps) => void; onTimeChange: ({ start, end }: DurationRange) => void; }) => { + const { startDate: startDateFromUrl, endDate: endDateFromUrl } = useActionHistoryUrlParams(); const getTestId = useTestIdGenerator('response-actions-list'); - const kibana = useKibana<IDataPluginServices>(); + const kibana = useKibana<IUnifiedSearchPluginServices>(); const { uiSettings } = kibana.services; const [commonlyUsedRanges] = useState(() => { return ( @@ -72,14 +75,22 @@ export const ActionLogDateRangePicker = memo( isLoading={isDataLoading} dateFormat={uiSettings.get('dateFormat')} commonlyUsedRanges={commonlyUsedRanges} - end={dateRangePickerState.endDate} + end={ + isFlyout + ? dateRangePickerState.endDate + : endDateFromUrl ?? dateRangePickerState.endDate + } isPaused={!dateRangePickerState.autoRefreshOptions.enabled} onTimeChange={onTimeChange} onRefreshChange={onRefreshChange} refreshInterval={dateRangePickerState.autoRefreshOptions.duration} onRefresh={onRefresh} recentlyUsedRanges={dateRangePickerState.recentlyUsedDateRanges} - start={dateRangePickerState.startDate} + start={ + isFlyout + ? dateRangePickerState.startDate + : startDateFromUrl ?? dateRangePickerState.startDate + } showUpdateButton={false} updateButtonProps={{ iconOnly: true, fill: false }} width="auto" diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filter.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filter.tsx index 9a5903a278fb7d..e26f1375984b59 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filter.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filter.tsx @@ -7,8 +7,9 @@ import React, { memo, useMemo, useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSelectable, EuiPopoverTitle } from '@elastic/eui'; +import type { ResponseActions } from '../../../../../common/endpoint/service/response_actions/constants'; import { ActionsLogFilterPopover } from './actions_log_filter_popover'; -import { type FilterItems, type FilterName, useActionsLogFilter } from './hooks'; +import { type FilterItems, type FilterName, useActionsLogFilter, getUiCommand } from './hooks'; import { ClearAllButton } from './clear_all_button'; import { UX_MESSAGES } from '../translations'; import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; @@ -16,22 +17,32 @@ import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; export const ActionsLogFilter = memo( ({ filterName, + isFlyout, onChangeFilterOptions, }: { filterName: FilterName; + isFlyout: boolean; onChangeFilterOptions: (selectedOptions: string[]) => void; }) => { const getTestId = useTestIdGenerator('response-actions-list'); - const { items, setItems, hasActiveFilters, numActiveFilters, numFilters } = - useActionsLogFilter(filterName); + const { + items, + setItems, + hasActiveFilters, + numActiveFilters, + numFilters, + setUrlActionsFilters, + setUrlStatusesFilters, + } = useActionsLogFilter(filterName, isFlyout); const isSearchable = useMemo(() => filterName !== 'statuses', [filterName]); const onChange = useCallback( (newOptions: FilterItems) => { - setItems(newOptions.map((e) => e)); + // update filter UI options state + setItems(newOptions.map((option) => option)); - // update selected filter state + // compute selected list of options const selectedItems = newOptions.reduce<string[]>((acc, curr) => { if (curr.checked === 'on') { acc.push(curr.key); @@ -39,22 +50,59 @@ export const ActionsLogFilter = memo( return acc; }, []); + if (!isFlyout) { + // update URL params + if (filterName === 'actions') { + setUrlActionsFilters( + selectedItems.map((item) => getUiCommand(item as ResponseActions)).join() + ); + } else if (filterName === 'statuses') { + setUrlStatusesFilters(selectedItems.join()); + } + } + // update query state onChangeFilterOptions(selectedItems); }, - [setItems, onChangeFilterOptions] + [ + filterName, + isFlyout, + setItems, + onChangeFilterOptions, + setUrlActionsFilters, + setUrlStatusesFilters, + ] ); // clear all selected options const onClearAll = useCallback(() => { + // update filter UI options state setItems( - items.map((e) => { - e.checked = undefined; - return e; + items.map((option) => { + option.checked = undefined; + return option; }) ); + + if (!isFlyout) { + // update URL params + if (filterName === 'actions') { + setUrlActionsFilters(''); + } else if (filterName === 'statuses') { + setUrlStatusesFilters(''); + } + } + // update query state onChangeFilterOptions([]); - }, [items, setItems, onChangeFilterOptions]); + }, [ + filterName, + isFlyout, + items, + setItems, + onChangeFilterOptions, + setUrlActionsFilters, + setUrlStatusesFilters, + ]); return ( <ActionsLogFilterPopover diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filters.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filters.tsx index 8512cc50dad296..9b65a2b33f752d 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filters.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filters.tsx @@ -22,6 +22,7 @@ export const ActionsLogFilters = memo( ({ dateRangePickerState, isDataLoading, + isFlyout, onClick, onChangeCommandsFilter, onChangeStatusesFilter, @@ -31,6 +32,7 @@ export const ActionsLogFilters = memo( }: { dateRangePickerState: DateRangePickerValues; isDataLoading: boolean; + isFlyout: boolean; onChangeCommandsFilter: (selectedCommands: string[]) => void; onChangeStatusesFilter: (selectedStatuses: string[]) => void; onRefresh: () => void; @@ -43,14 +45,19 @@ export const ActionsLogFilters = memo( // TODO: add more filter names here (users, hosts, statuses) return ( <> - <ActionsLogFilter filterName={'actions'} onChangeFilterOptions={onChangeCommandsFilter} /> + <ActionsLogFilter + filterName={'actions'} + isFlyout={isFlyout} + onChangeFilterOptions={onChangeCommandsFilter} + /> <ActionsLogFilter filterName={'statuses'} + isFlyout={isFlyout} onChangeFilterOptions={onChangeStatusesFilter} /> </> ); - }, [onChangeCommandsFilter, onChangeStatusesFilter]); + }, [isFlyout, onChangeCommandsFilter, onChangeStatusesFilter]); const onClickRefreshButton = useCallback(() => onClick(), [onClick]); @@ -63,6 +70,7 @@ export const ActionsLogFilters = memo( <ActionLogDateRangePicker dateRangePickerState={dateRangePickerState} isDataLoading={isDataLoading} + isFlyout={isFlyout} onRefresh={onRefresh} onRefreshChange={onRefreshChange} onTimeChange={onTimeChange} diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx index 4bf28276d16515..4552d3912f8421 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx @@ -10,7 +10,10 @@ import type { DurationRange, OnRefreshChangeProps, } from '@elastic/eui/src/components/date_picker/types'; -import type { ResponseActionStatus } from '../../../../../common/endpoint/service/response_actions/constants'; +import type { + ResponseActions, + ResponseActionStatus, +} from '../../../../../common/endpoint/service/response_actions/constants'; import { RESPONSE_ACTION_COMMANDS, RESPONSE_ACTION_STATUS, @@ -19,6 +22,7 @@ import type { DateRangePickerValues } from './actions_log_date_range_picker'; import type { FILTER_NAMES } from '../translations'; import { UX_MESSAGES } from '../translations'; import { StatusBadge } from './status_badge'; +import { useActionHistoryUrlParams } from './use_action_history_url_params'; const defaultDateRangeOptions = Object.freeze({ autoRefreshOptions: { @@ -30,7 +34,8 @@ const defaultDateRangeOptions = Object.freeze({ recentlyUsedDateRanges: [], }); -export const useDateRangePicker = () => { +export const useDateRangePicker = (isFlyout: boolean) => { + const { setUrlDateRangeFilters } = useActionHistoryUrlParams(); const [dateRangePickerState, setDateRangePickerState] = useState<DateRangePickerValues>(defaultDateRangeOptions); @@ -83,9 +88,16 @@ export const useDateRangePicker = () => { .slice(0, 9), ]; updateActionListRecentlyUsedDateRanges(newRecentlyUsedDateRanges); + + // update URL params for date filters + if (!isFlyout) { + setUrlDateRangeFilters({ startDate: newStart, endDate: newEnd }); + } }, [ dateRangePickerState.recentlyUsedDateRanges, + isFlyout, + setUrlDateRangeFilters, updateActionListDateRanges, updateActionListRecentlyUsedDateRanges, ] @@ -98,6 +110,7 @@ export type FilterItems = Array<{ key: string; label: string; checked: 'on' | undefined; + 'data-test-subj': string; }>; export const getActionStatus = (status: ResponseActionStatus): string => { @@ -111,36 +124,84 @@ export const getActionStatus = (status: ResponseActionStatus): string => { return ''; }; +/** + * map actual command to ui command + * unisolate -> release + * running-processes -> processes + */ +export const getUiCommand = ( + command: ResponseActions +): Exclude<ResponseActions, 'unisolate' | 'running-processes'> | 'release' | 'processes' => { + if (command === 'unisolate') { + return 'release'; + } else if (command === 'running-processes') { + return 'processes'; + } else { + return command; + } +}; + +/** + * map UI command back to actual command + * release -> unisolate + * processes -> running-processes + */ +export const getCommandKey = ( + uiCommand: Exclude<ResponseActions, 'unisolate' | 'running-processes'> | 'release' | 'processes' +): ResponseActions => { + if (uiCommand === 'release') { + return 'unisolate'; + } else if (uiCommand === 'processes') { + return 'running-processes'; + } else { + return uiCommand; + } +}; + // TODO: add more filter names here export type FilterName = keyof typeof FILTER_NAMES; export const useActionsLogFilter = ( - filterName: FilterName + filterName: FilterName, + isFlyout: boolean ): { items: FilterItems; setItems: React.Dispatch<React.SetStateAction<FilterItems>>; hasActiveFilters: boolean; numActiveFilters: number; numFilters: number; + setUrlActionsFilters: ReturnType<typeof useActionHistoryUrlParams>['setUrlActionsFilters']; + setUrlStatusesFilters: ReturnType<typeof useActionHistoryUrlParams>['setUrlStatusesFilters']; } => { + const { commands, statuses, setUrlActionsFilters, setUrlStatusesFilters } = + useActionHistoryUrlParams(); const isStatusesFilter = filterName === 'statuses'; const [items, setItems] = useState<FilterItems>( isStatusesFilter - ? RESPONSE_ACTION_STATUS.map((filter) => ({ - key: filter, + ? RESPONSE_ACTION_STATUS.map((statusName) => ({ + key: statusName, label: ( <StatusBadge color={ - filter === 'successful' ? 'success' : filter === 'failed' ? 'danger' : 'warning' + statusName === 'successful' + ? 'success' + : statusName === 'failed' + ? 'danger' + : 'warning' } - status={getActionStatus(filter)} + status={getActionStatus(statusName)} /> ) as unknown as string, - checked: undefined, + checked: !isFlyout && statuses?.includes(statusName) ? 'on' : undefined, + 'data-test-subj': `${filterName}-filter-option`, })) - : RESPONSE_ACTION_COMMANDS.map((filter) => ({ - key: filter, - label: filter === 'unisolate' ? 'release' : filter, - checked: undefined, + : RESPONSE_ACTION_COMMANDS.map((commandName) => ({ + key: commandName, + label: getUiCommand(commandName), + checked: + !isFlyout && commands?.map((command) => getCommandKey(command)).includes(commandName) + ? 'on' + : undefined, + 'data-test-subj': `${filterName}-filter-option`, })) ); @@ -151,5 +212,13 @@ export const useActionsLogFilter = ( ); const numFilters = useMemo(() => items.filter((item) => item.checked !== 'on').length, [items]); - return { items, setItems, hasActiveFilters, numActiveFilters, numFilters }; + return { + items, + setItems, + hasActiveFilters, + numActiveFilters, + numFilters, + setUrlActionsFilters, + setUrlStatusesFilters, + }; }; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/use_action_history_url_params.test.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/use_action_history_url_params.test.ts new file mode 100644 index 00000000000000..376adee67bb360 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/use_action_history_url_params.test.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 { actionsLogFiltersFromUrlParams } from './use_action_history_url_params'; + +describe('#actionsLogFiltersFromUrlParams', () => { + it('should not use invalid command values to URL params', () => { + expect(actionsLogFiltersFromUrlParams({ commands: 'asa,was' })).toEqual({ + commands: undefined, + endDate: undefined, + hosts: undefined, + startDate: undefined, + statuses: undefined, + users: undefined, + }); + }); + + it('should use valid command values to URL params', () => { + expect( + actionsLogFiltersFromUrlParams({ + commands: 'kill-process,isolate,processes,release,suspend-process', + }) + ).toEqual({ + commands: ['isolate', 'kill-process', 'processes', 'release', 'suspend-process'], + endDate: undefined, + hosts: undefined, + startDate: undefined, + statuses: undefined, + users: undefined, + }); + }); + + it('should not use invalid status values to URL params', () => { + expect(actionsLogFiltersFromUrlParams({ statuses: 'asa,was' })).toEqual({ + commands: undefined, + endDate: undefined, + hosts: undefined, + startDate: undefined, + statuses: undefined, + users: undefined, + }); + }); + + it('should use valid status values to URL params', () => { + expect( + actionsLogFiltersFromUrlParams({ + statuses: 'successful,pending,failed', + }) + ).toEqual({ + commands: undefined, + endDate: undefined, + hosts: undefined, + startDate: undefined, + statuses: ['failed', 'pending', 'successful'], + users: undefined, + }); + }); + + it('should use valid command and status values to URL params', () => { + expect( + actionsLogFiltersFromUrlParams({ + commands: 'release,kill-process,isolate,processes,suspend-process', + statuses: 'successful,pending,failed', + }) + ).toEqual({ + commands: ['isolate', 'kill-process', 'processes', 'release', 'suspend-process'], + endDate: undefined, + hosts: undefined, + startDate: undefined, + statuses: ['failed', 'pending', 'successful'], + users: undefined, + }); + }); + + it('should use set given relative startDate and endDate values to URL params', () => { + expect( + actionsLogFiltersFromUrlParams({ + startDate: 'now-24h/h', + endDate: 'now', + }) + ).toEqual({ + commands: undefined, + endDate: 'now', + hosts: undefined, + startDate: 'now-24h/h', + statuses: undefined, + users: undefined, + }); + }); + + it('should use set given absolute startDate and endDate values to URL params', () => { + expect( + actionsLogFiltersFromUrlParams({ + startDate: '2022-09-12T08:00:00.000Z', + endDate: '2022-09-12T08:30:33.140Z', + }) + ).toEqual({ + commands: undefined, + endDate: '2022-09-12T08:30:33.140Z', + hosts: undefined, + startDate: '2022-09-12T08:00:00.000Z', + statuses: undefined, + users: undefined, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/use_action_history_url_params.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/use_action_history_url_params.ts new file mode 100644 index 00000000000000..24d728bd5c1893 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/use_action_history_url_params.ts @@ -0,0 +1,194 @@ +/* + * 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 { useCallback, useEffect, useMemo, useState } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import { + RESPONSE_ACTION_COMMANDS, + RESPONSE_ACTION_STATUS, + type ResponseActions, + type ResponseActionStatus, +} from '../../../../../common/endpoint/service/response_actions/constants'; +import { useUrlParams } from '../../../hooks/use_url_params'; + +interface UrlParamsActionsLogFilters { + commands: string; + hosts: string; + statuses: string; + startDate: string; + endDate: string; + users: string; +} + +interface ActionsLogFiltersFromUrlParams { + commands?: Array< + Exclude<ResponseActions, 'unisolate' | 'running-processes'> | 'release' | 'processes' + >; + hosts?: string[]; + statuses?: ResponseActionStatus[]; + startDate?: string; + endDate?: string; + setUrlActionsFilters: (commands: UrlParamsActionsLogFilters['commands']) => void; + setUrlDateRangeFilters: ({ startDate, endDate }: { startDate: string; endDate: string }) => void; + setUrlHostsFilters: (agentIds: UrlParamsActionsLogFilters['hosts']) => void; + setUrlStatusesFilters: (statuses: UrlParamsActionsLogFilters['statuses']) => void; + setUrlUsersFilters: (users: UrlParamsActionsLogFilters['users']) => void; + users?: string[]; +} + +type FiltersFromUrl = Pick< + ActionsLogFiltersFromUrlParams, + 'commands' | 'hosts' | 'statuses' | 'users' | 'startDate' | 'endDate' +>; + +export const actionsLogFiltersFromUrlParams = ( + urlParams: Partial<UrlParamsActionsLogFilters> +): FiltersFromUrl => { + const actionsLogFilters: FiltersFromUrl = { + commands: [], + hosts: [], + statuses: [], + startDate: 'now-24h/h', + endDate: 'now', + users: [], + }; + + const urlCommands = urlParams.commands + ? String(urlParams.commands) + .split(',') + .reduce<Required<ActionsLogFiltersFromUrlParams>['commands']>((acc, curr) => { + if ( + RESPONSE_ACTION_COMMANDS.includes(curr as ResponseActions) || + curr === 'release' || + curr === 'processes' + ) { + acc.push(curr as Required<ActionsLogFiltersFromUrlParams>['commands'][number]); + } + return acc.sort(); + }, []) + : []; + + const urlHosts = urlParams.hosts ? String(urlParams.hosts).split(',').sort() : []; + + const urlStatuses = urlParams.statuses + ? (String(urlParams.statuses).split(',') as ResponseActionStatus[]).reduce< + ResponseActionStatus[] + >((acc, curr) => { + if (RESPONSE_ACTION_STATUS.includes(curr)) { + acc.push(curr); + } + return acc.sort(); + }, []) + : []; + + const urlUsers = urlParams.hosts ? String(urlParams.users).split(',').sort() : []; + + actionsLogFilters.commands = urlCommands.length ? urlCommands : undefined; + actionsLogFilters.hosts = urlHosts.length ? urlHosts : undefined; + actionsLogFilters.statuses = urlStatuses.length ? urlStatuses : undefined; + actionsLogFilters.startDate = urlParams.startDate ? String(urlParams.startDate) : undefined; + actionsLogFilters.endDate = urlParams.endDate ? String(urlParams.endDate) : undefined; + actionsLogFilters.users = urlUsers.length ? urlUsers : undefined; + + return actionsLogFilters; +}; + +export const useActionHistoryUrlParams = (): ActionsLogFiltersFromUrlParams => { + // track actions and status filters + const location = useLocation(); + const history = useHistory(); + const { urlParams, toUrlParams } = useUrlParams(); + + const getUrlActionsLogFilters = useMemo( + () => actionsLogFiltersFromUrlParams(urlParams), + [urlParams] + ); + const [actionsLogFilters, setActionsLogFilters] = useState(getUrlActionsLogFilters); + + const setUrlActionsFilters = useCallback( + (commands: string) => { + history.push({ + ...location, + search: toUrlParams({ + ...urlParams, + commands: commands.length ? commands : undefined, + }), + }); + }, + [history, location, toUrlParams, urlParams] + ); + + const setUrlHostsFilters = useCallback( + (agentIds: string) => { + history.push({ + ...location, + search: toUrlParams({ + ...urlParams, + hosts: agentIds.length ? agentIds : undefined, + }), + }); + }, + [history, location, toUrlParams, urlParams] + ); + + const setUrlStatusesFilters = useCallback( + (statuses: string) => { + history.push({ + ...location, + search: toUrlParams({ + ...urlParams, + statuses: statuses.length ? statuses : undefined, + }), + }); + }, + [history, location, toUrlParams, urlParams] + ); + + const setUrlUsersFilters = useCallback( + (users: string) => { + history.push({ + ...location, + search: toUrlParams({ + ...urlParams, + users: users.length ? users : undefined, + }), + }); + }, + [history, location, toUrlParams, urlParams] + ); + + const setUrlDateRangeFilters = useCallback( + ({ startDate, endDate }: { startDate: string; endDate: string }) => { + history.push({ + ...location, + search: toUrlParams({ + ...urlParams, + startDate: startDate.length ? startDate : undefined, + endDate: endDate.length ? endDate : undefined, + }), + }); + }, + [history, location, toUrlParams, urlParams] + ); + + useEffect(() => { + setActionsLogFilters((prevState) => { + return { + ...prevState, + ...actionsLogFiltersFromUrlParams(urlParams), + }; + }); + }, [setActionsLogFilters, urlParams]); + + return { + ...actionsLogFilters, + setUrlActionsFilters, + setUrlDateRangeFilters, + setUrlHostsFilters, + setUrlStatusesFilters, + setUrlUsersFilters, + }; +}; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/mocks.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/mocks.tsx new file mode 100644 index 00000000000000..65b40d9a924ea1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/mocks.tsx @@ -0,0 +1,75 @@ +/* + * 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 uuid from 'uuid'; +import type { ActionListApiResponse } from '../../../../common/endpoint/types'; +import type { ResponseActionStatus } from '../../../../common/endpoint/service/response_actions/constants'; +import { EndpointActionGenerator } from '../../../../common/endpoint/data_generators/endpoint_action_generator'; + +export const getActionListMock = async ({ + agentIds: _agentIds, + commands, + actionCount = 0, + endDate, + page = 1, + pageSize = 10, + startDate, + userIds, + isCompleted = true, + isExpired = false, + wasSuccessful = true, + status = 'successful', +}: { + agentIds?: string[]; + commands?: string[]; + actionCount?: number; + endDate?: string; + page?: number; + pageSize?: number; + startDate?: string; + userIds?: string[]; + isCompleted?: boolean; + isExpired?: boolean; + wasSuccessful?: boolean; + status?: ResponseActionStatus; +}): Promise<ActionListApiResponse> => { + const endpointActionGenerator = new EndpointActionGenerator('seed'); + + const agentIds = _agentIds ?? [uuid.v4()]; + + const data: ActionListApiResponse['data'] = agentIds.map((id) => { + const actionIds = Array(actionCount) + .fill(1) + .map(() => uuid.v4()); + + const actionDetails: ActionListApiResponse['data'] = actionIds.map((actionId) => { + return endpointActionGenerator.generateActionDetails({ + agents: [id], + id: actionId, + isCompleted, + isExpired, + wasSuccessful, + status, + completedAt: isExpired ? undefined : new Date().toISOString(), + }); + }); + return actionDetails; + })[0]; + + return { + page, + pageSize, + startDate, + endDate, + elasticAgentIds: agentIds, + commands, + data, + userIds, + statuses: undefined, + total: data.length ?? 0, + }; +}; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx index 1b97987d4a1313..64910663ac5cf1 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx @@ -5,18 +5,18 @@ * 2.0. */ -import uuid from 'uuid'; import React from 'react'; import * as reactTestingLibrary from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; -import type { AppContextTestRender } from '../../../common/mock/endpoint'; -import { createAppRootMockRenderer } from '../../../common/mock/endpoint'; +import { + createAppRootMockRenderer, + type AppContextTestRender, +} from '../../../common/mock/endpoint'; import { ResponseActionsLog } from './response_actions_log'; import type { ActionListApiResponse } from '../../../../common/endpoint/types'; -import type { ResponseActionStatus } from '../../../../common/endpoint/service/response_actions/constants'; import { MANAGEMENT_PATH } from '../../../../common/constants'; -import { EndpointActionGenerator } from '../../../../common/endpoint/data_generators/endpoint_action_generator'; +import { getActionListMock } from './mocks'; let mockUseGetEndpointActionList: { isFetched?: boolean; @@ -488,7 +488,7 @@ describe('Response Actions Log', () => { expect(filterList.querySelectorAll('ul>li').length).toEqual(5); expect( Array.from(filterList.querySelectorAll('ul>li')).map((option) => option.textContent) - ).toEqual(['isolate', 'release', 'kill-process', 'suspend-process', 'running-processes']); + ).toEqual(['isolate', 'release', 'kill-process', 'suspend-process', 'processes']); }); it('should have `clear all` button `disabled` when no selected values', () => { @@ -500,69 +500,28 @@ describe('Response Actions Log', () => { expect(clearAllButton.hasAttribute('disabled')).toBeTruthy(); }); }); -}); -// mock API response -const getActionListMock = async ({ - agentIds: _agentIds, - commands, - actionCount = 0, - endDate, - page = 1, - pageSize = 10, - startDate, - userIds, - isCompleted = true, - isExpired = false, - wasSuccessful = true, - status = 'successful', -}: { - agentIds?: string[]; - commands?: string[]; - actionCount?: number; - endDate?: string; - page?: number; - pageSize?: number; - startDate?: string; - userIds?: string[]; - isCompleted?: boolean; - isExpired?: boolean; - wasSuccessful?: boolean; - status?: ResponseActionStatus; -}): Promise<ActionListApiResponse> => { - const endpointActionGenerator = new EndpointActionGenerator('seed'); - - const agentIds = _agentIds ?? [uuid.v4()]; - - const data: ActionListApiResponse['data'] = agentIds.map((id) => { - const actionIds = Array(actionCount) - .fill(1) - .map(() => uuid.v4()); - - const actionDetails: ActionListApiResponse['data'] = actionIds.map((actionId) => { - return endpointActionGenerator.generateActionDetails({ - agents: [id], - id: actionId, - isCompleted, - isExpired, - wasSuccessful, - status, - completedAt: isExpired ? undefined : new Date().toISOString(), - }); + describe('Statuses filter', () => { + const filterPrefix = '-statuses-filter'; + + it('should show a list of statuses when opened', () => { + render(); + userEvent.click(renderResult.getByTestId(`${testPrefix}${filterPrefix}-popoverButton`)); + const filterList = renderResult.getByTestId(`${testPrefix}${filterPrefix}-popoverList`); + expect(filterList).toBeTruthy(); + expect(filterList.querySelectorAll('ul>li').length).toEqual(3); + expect( + Array.from(filterList.querySelectorAll('ul>li')).map((option) => option.textContent) + ).toEqual(['Failed', 'Pending', 'Successful']); }); - return actionDetails; - })[0]; - return { - page, - pageSize, - startDate, - endDate, - elasticAgentIds: agentIds, - commands, - data, - userIds, - statuses: undefined, - total: data.length ?? 0, - }; -}; + it('should have `clear all` button `disabled` when no selected values', () => { + render(); + userEvent.click(renderResult.getByTestId(`${testPrefix}${filterPrefix}-popoverButton`)); + const clearAllButton = renderResult.getByTestId( + `${testPrefix}${filterPrefix}-clearAllButton` + ); + expect(clearAllButton.hasAttribute('disabled')).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.tsx index 8f873da6d9232e..79c99d62e2ff6e 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.tsx @@ -25,7 +25,7 @@ import { import { euiStyled, css } from '@kbn/kibana-react-plugin/common'; import type { HorizontalAlignment, CriteriaWithPagination } from '@elastic/eui'; -import React, { memo, useCallback, useMemo, useState } from 'react'; +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import type { ResponseActions, @@ -41,14 +41,18 @@ import { OUTPUT_MESSAGES, TABLE_COLUMN_NAMES, UX_MESSAGES } from './translations import { MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../common/constants'; import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; import { ActionsLogFilters } from './components/actions_log_filters'; -import { getActionStatus, useDateRangePicker } from './components/hooks'; +import { + getActionStatus, + getUiCommand, + getCommandKey, + useDateRangePicker, +} from './components/hooks'; import { StatusBadge } from './components/status_badge'; +import { useActionHistoryUrlParams } from './components/use_action_history_url_params'; +import { useUrlPagination } from '../../hooks/use_url_pagination'; const emptyValue = getEmptyValue(); -const getCommand = (command: ResponseActions): Exclude<ResponseActions, 'unisolate'> | 'release' => - command === 'unisolate' ? 'release' : command; - // Truncated usernames const StyledFacetButton = euiStyled(EuiFacetButton)` .euiText { @@ -107,24 +111,48 @@ const StyledEuiCodeBlock = euiStyled(EuiCodeBlock).attrs({ `; export const ResponseActionsLog = memo< - Pick<EndpointActionListRequestQuery, 'agentIds'> & { showHostNames?: boolean } ->(({ agentIds, showHostNames = false }) => { + Pick<EndpointActionListRequestQuery, 'agentIds'> & { showHostNames?: boolean; isFlyout?: boolean } +>(({ agentIds, showHostNames = false, isFlyout = true }) => { + const { pagination: paginationFromUrlParams, setPagination: setPaginationOnUrlParams } = + useUrlPagination(); + const { + commands: commandsFromUrl, + statuses: statusesFromUrl, + startDate: startDateFromUrl, + endDate: endDateFromUrl, + } = useActionHistoryUrlParams(); + const getTestId = useTestIdGenerator('response-actions-list'); const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<{ [k: ActionListApiResponse['data'][number]['id']]: React.ReactNode; }>({}); const [queryParams, setQueryParams] = useState<EndpointActionListRequestQuery>({ - page: 1, - pageSize: 10, + page: isFlyout ? 1 : paginationFromUrlParams.page, + pageSize: isFlyout ? 10 : paginationFromUrlParams.pageSize, agentIds, commands: [], statuses: [], userIds: [], }); + // update query state from URL params + useEffect(() => { + if (!isFlyout) { + setQueryParams((prevState) => ({ + ...prevState, + commands: commandsFromUrl?.length + ? commandsFromUrl.map((commandFromUrl) => getCommandKey(commandFromUrl)) + : prevState.commands, + statuses: statusesFromUrl?.length + ? (statusesFromUrl as ResponseActionStatus[]) + : prevState.statuses, + })); + } + }, [commandsFromUrl, isFlyout, statusesFromUrl, setQueryParams]); + // date range picker state and handlers - const { dateRangePickerState, onRefreshChange, onTimeChange } = useDateRangePicker(); + const { dateRangePickerState, onRefreshChange, onTimeChange } = useDateRangePicker(isFlyout); // initial fetch of list data const { @@ -135,8 +163,8 @@ export const ResponseActionsLog = memo< refetch: reFetchEndpointActionList, } = useGetEndpointActionList({ ...queryParams, - startDate: dateRangePickerState.startDate, - endDate: dateRangePickerState.endDate, + startDate: isFlyout ? dateRangePickerState.startDate : startDateFromUrl, + endDate: isFlyout ? dateRangePickerState.endDate : endDateFromUrl, }); // handle auto refresh data @@ -194,7 +222,7 @@ export const ResponseActionsLog = memo< }) : undefined; - const command = getCommand(_command); + const command = getUiCommand(_command); const dataList = [ { title: OUTPUT_MESSAGES.expandSection.placedAt, @@ -297,14 +325,16 @@ export const ResponseActionsLog = memo< width: !showHostNames ? '21%' : '10%', truncateText: true, render: (_command: ActionListApiResponse['data'][number]['command']) => { - const command = getCommand(_command); + const command = getUiCommand(_command); return ( <EuiToolTip content={command} anchorClassName="eui-textTruncate"> - <FormattedMessage - id="xpack.securitySolution.responseActionsList.list.item.command" - defaultMessage="{command}" - values={{ command }} - /> + <EuiText + size="s" + className="eui-textTruncate eui-fullWidth" + data-test-subj={getTestId('column-command')} + > + {command} + </EuiText> </EuiToolTip> ); }, @@ -451,31 +481,53 @@ export const ResponseActionsLog = memo< // table pagination const tablePagination = useMemo(() => { + const pageIndex = isFlyout ? (queryParams.page || 1) - 1 : paginationFromUrlParams.page - 1; + const pageSize = isFlyout ? queryParams.pageSize || 10 : paginationFromUrlParams.pageSize; return { // this controls the table UI page // to match 0-based table paging - pageIndex: (queryParams.page || 1) - 1, - pageSize: queryParams.pageSize || 10, + pageIndex, + pageSize, totalItemCount, pageSizeOptions: MANAGEMENT_PAGE_SIZE_OPTIONS as number[], }; - }, [queryParams, totalItemCount]); + }, [ + isFlyout, + paginationFromUrlParams.page, + paginationFromUrlParams.pageSize, + queryParams.page, + queryParams.pageSize, + totalItemCount, + ]); // handle onChange const handleTableOnChange = useCallback( ({ page: _page }: CriteriaWithPagination<ActionListApiResponse['data'][number]>) => { // table paging is 0 based const { index, size } = _page; - setQueryParams((prevState) => ({ - ...prevState, - // adjust the page to conform to - // 1-based API page + // adjust the page to conform to + // 1-based API page + const pagingArgs = { page: index + 1, pageSize: size, - })); + }; + if (isFlyout) { + setQueryParams((prevState) => ({ + ...prevState, + ...pagingArgs, + })); + } else { + setQueryParams((prevState) => ({ + ...prevState, + ...pagingArgs, + })); + setPaginationOnUrlParams({ + ...pagingArgs, + }); + } reFetchEndpointActionList(); }, - [reFetchEndpointActionList, setQueryParams] + [isFlyout, reFetchEndpointActionList, setQueryParams, setPaginationOnUrlParams] ); // compute record ranges @@ -517,6 +569,7 @@ export const ResponseActionsLog = memo< return ( <> <ActionsLogFilters + isFlyout={isFlyout} dateRangePickerState={dateRangePickerState} isDataLoading={isFetching} onClick={reFetchEndpointActionList} diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/translations.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/translations.tsx index fc6d3f6e7349ec..f16feaeb944559 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/translations.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/translations.tsx @@ -95,8 +95,8 @@ export const UX_MESSAGES = Object.freeze({ defaultMessage: `Actions log : {hostname}`, values: { hostname }, }), - pageTitle: i18n.translate('xpack.securitySolution.responseActionsList.list.title', { - defaultMessage: 'Response actions', + pageSubTitle: i18n.translate('xpack.securitySolution.responseActionsList.list.pageSubTitle', { + defaultMessage: 'View the history of response actions performed on hosts.', }), fetchError: i18n.translate('xpack.securitySolution.responseActionsList.list.errorMessage', { defaultMessage: 'Error while retrieving response actions', diff --git a/x-pack/plugins/security_solution/public/management/components/package_action_item/package_action_formatter.ts b/x-pack/plugins/security_solution/public/management/components/package_action_item/package_action_formatter.ts new file mode 100644 index 00000000000000..f7796f8ef4c5c6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/package_action_item/package_action_formatter.ts @@ -0,0 +1,104 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import type { DocLinks } from '@kbn/doc-links'; + +type PackageActions = 'es_connection' | 'policy_failure'; + +export const titles = Object.freeze( + new Map<PackageActions, string>([ + [ + 'es_connection', + i18n.translate('xpack.securitySolution.endpoint.details.packageActions.es_connection.title', { + defaultMessage: 'Elasticsearch connection failure', + }), + ], + [ + 'policy_failure', + i18n.translate( + 'xpack.securitySolution.endpoint.details.packageActions.policy_failure.title', + { + defaultMessage: 'Policy response failure', + } + ), + ], + ]) +); + +export const descriptions = Object.freeze( + new Map<Partial<PackageActions> | string, string>([ + [ + 'es_connection', + i18n.translate( + 'xpack.securitySolution.endpoint.details.packageActions.es_connection.description', + { + defaultMessage: + "The endpoint's connection to Elasticsearch is either down or misconfigured. Make sure it is configured correctly.", + } + ), + ], + [ + 'policy_failure', + i18n.translate( + 'xpack.securitySolution.endpoint.details.packageActions.policy_failure.description', + { + defaultMessage: + 'The Endpoint did not apply the Policy correctly. Expand the Policy response above for more details.', + } + ), + ], + ]) +); + +const linkTexts = Object.freeze( + new Map<Partial<PackageActions> | string, string>([ + [ + 'es_connection', + i18n.translate( + 'xpack.securitySolution.endpoint.details.packageActions.link.text.es_connection', + { + defaultMessage: ' Read more.', + } + ), + ], + ]) +); + +export class PackageActionFormatter { + public key: PackageActions; + public title: string; + public description: string; + public linkText?: string; + + constructor( + code: number, + message: string, + private docLinks: DocLinks['securitySolution']['packageActionTroubleshooting'] + ) { + this.key = this.getKeyFromErrorCode(code); + this.title = titles.get(this.key) ?? this.key; + this.description = descriptions.get(this.key) || message; + this.linkText = linkTexts.get(this.key); + } + + public get linkUrl(): string { + return this.docLinks[ + this.key as keyof DocLinks['securitySolution']['packageActionTroubleshooting'] + ]; + } + + private getKeyFromErrorCode(code: number): PackageActions { + if (code === 123) { + return 'es_connection'; + } else if (code === 124) { + return 'policy_failure'; + } else { + throw new Error(`Invalid error code ${code}`); + } + } +} diff --git a/x-pack/plugins/security_solution/public/management/components/package_action_item/package_action_item_error.tsx b/x-pack/plugins/security_solution/public/management/components/package_action_item/package_action_item_error.tsx new file mode 100644 index 00000000000000..63e3c2d5488399 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/package_action_item/package_action_item_error.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import styled from 'styled-components'; +import { EuiLink, EuiCallOut, EuiText } from '@elastic/eui'; +import type { PackageActionFormatter } from './package_action_formatter'; + +const StyledEuiCallout = styled(EuiCallOut)` + padding: ${({ theme }) => theme.eui.euiSizeS}; +`; + +const StyledEuiText = styled(EuiText)` + white-space: break-spaces; + text-align: left; + line-height: inherit; +`; + +interface PackageActionItemErrorProps { + actionFormatter: PackageActionFormatter; +} +/** + * A package action item error + */ +export const PackageActionItemError = memo(({ actionFormatter }: PackageActionItemErrorProps) => { + return ( + <StyledEuiCallout + title={actionFormatter.title} + color="danger" + iconType="alert" + data-test-subj="packageItemErrorCallOut" + > + <StyledEuiText size="s" data-test-subj="packageItemErrorCallOutMessage"> + {actionFormatter.description} + {actionFormatter.linkText && actionFormatter.linkUrl && ( + <EuiLink + target="_blank" + href={actionFormatter.linkUrl} + data-test-subj="packageItemErrorCallOutLink" + > + {actionFormatter.linkText} + </EuiLink> + )} + </StyledEuiText> + </StyledEuiCallout> + ); +}); + +PackageActionItemError.displayName = 'PackageActionItemError'; diff --git a/x-pack/plugins/security_solution/public/management/icons/action_history.tsx b/x-pack/plugins/security_solution/public/management/icons/action_history.tsx new file mode 100644 index 00000000000000..9a2763a2f338fb --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/icons/action_history.tsx @@ -0,0 +1,37 @@ +/* + * 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 { SVGProps } from 'react'; +import React from 'react'; +export const IconActionHistory: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => ( + <svg + xmlns="http://www.w3.org/2000/svg" + width={16} + height={16} + fill="none" + viewBox="0 0 32 32" + {...props} + > + <path + fillRule="evenodd" + clipRule="evenodd" + fill="#535766" + d="M29 9H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h26a3 3 0 0 1 3 3v3a3 3 0 0 1-3 3zM3 2a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h26a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z" + /> + <path + fillRule="evenodd" + clipRule="evenodd" + fill="#00BFB3" + d="M29 32H3a3 3 0 0 1-3-3V14a3 3 0 0 1 3-3h26a3 3 0 0 1 3 3v15a3 3 0 0 1-3 3zM3 13a1 1 0 0 0-1 1v15a1 1 0 0 0 1 1h26a1 1 0 0 0 1-1V14a1 1 0 0 0-1-1H3z" + /> + <path + fillRule="evenodd" + clipRule="evenodd" + fill="#535766" + d="m7.29 17.71 3.3 3.29-3.3 3.29 1.42 1.42 4.7-4.71-4.7-4.71zM15 24h9v2h-9z" + /> + </svg> +); diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index 4f41f95d3a5568..659fb7a8216a51 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -16,6 +16,7 @@ import { HOST_ISOLATION_EXCEPTIONS_PATH, MANAGE_PATH, POLICIES_PATH, + ACTION_HISTORY_PATH, RULES_CREATE_PATH, RULES_PATH, SecurityPageName, @@ -31,6 +32,7 @@ import { HOST_ISOLATION_EXCEPTIONS, MANAGE, POLICIES, + ACTION_HISTORY, RULES, TRUSTED_APPLICATIONS, } from '../app/translations'; @@ -41,6 +43,7 @@ import { manageCategories as cloudSecurityPostureCategories, manageLinks as cloudSecurityPostureLinks, } from '../cloud_security_posture/links'; +import { IconActionHistory } from './icons/action_history'; import { IconBlocklist } from './icons/blocklist'; import { IconEndpoints } from './icons/endpoints'; import { IconEndpointPolicies } from './icons/endpoint_policies'; @@ -69,6 +72,7 @@ const categories = [ SecurityPageName.eventFilters, SecurityPageName.hostIsolationExceptions, SecurityPageName.blocklist, + SecurityPageName.actionHistory, ], }, ...cloudSecurityPostureCategories, @@ -202,6 +206,17 @@ export const links: LinkItem = { skipUrlState: true, hideTimeline: true, }, + { + id: SecurityPageName.actionHistory, + title: ACTION_HISTORY, + description: i18n.translate('xpack.securitySolution.appLinks.actionHistoryDescription', { + defaultMessage: 'View the history of response actions performed on hosts.', + }), + landingIcon: IconActionHistory, + path: ACTION_HISTORY_PATH, + skipUrlState: true, + hideTimeline: true, + }, cloudSecurityPostureLinks, ], }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index 7d1fd0a3d77fee..c2cecdba29b3d3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -255,15 +255,15 @@ describe('endpoint list middleware', () => { query: { agent_ids: [ '0dc3661d-6e67-46b0-af39-6f12b025fcb0', - 'a8e32a61-2685-47f0-83eb-edf157b8e616', - '37e219a8-fe16-4da9-bf34-634c5824b484', - '2484eb13-967e-4491-bf83-dffefdfe607c', - '0bc08ef6-6d6a-4113-92f2-b97811187c63', - 'f4127d87-b567-4a6e-afa6-9a1c7dc95f01', - 'f9ab5b8c-a43e-4e80-99d6-11570845a697', - '406c4b6a-ca57-4bd1-bc66-d9d999df3e70', - '2da1dd51-f7af-4f0e-b64c-e7751c74b0e7', - '89a94ea4-073c-4cb6-90a2-500805837027', + '34634c58-24b4-4448-80f4-107fb9918494', + '5a1298e3-e607-4bc0-8ef6-6d6a811312f2', + '78c54b13-596d-4891-95f4-80092d04454b', + '445f1fd2-5f81-4ddd-bdb6-f0d1bf2efe90', + 'd77a3fc6-3096-4852-a6ee-f6b09278fbc6', + '892fcccf-1bd8-45a2-a9cc-9a7860a3cb81', + '693a3110-5ba0-4284-a264-5d78301db08c', + '554db084-64fa-4e4a-ba47-2ba713f9932b', + 'c217deb6-674d-4f97-bb1d-a3a04238e6d7', ], }, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.test.tsx index a2b7a8ad2ce2f0..57611fcdf24842 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.test.tsx @@ -16,7 +16,7 @@ import { fireEvent } from '@testing-library/dom'; import { uiQueryParams } from '../../store/selectors'; import type { EndpointIndexUIQueryParams } from '../../types'; -// FLAKY: https://github.com/elastic/kibana/issues/132398 +// FLAKY: https://github.com/elastic/kibana/issues/140618 describe.skip('when rendering the endpoint list `AdminSearchBar`', () => { let render: ( urlParams?: EndpointIndexUIQueryParams @@ -85,7 +85,8 @@ describe.skip('when rendering the endpoint list `AdminSearchBar`', () => { expect(getQueryParamsFromStore().admin_query).toBe("(language:kuery,query:'host.name: foo')"); }); - it.each([ + // FLAKY: https://github.com/elastic/kibana/issues/132398 + it.skip.each([ ['nothing', ''], ['spaces', ' '], ])( diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx index b60cdf6040b1d6..23f8ea83a70941 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx @@ -85,7 +85,8 @@ describe('When on the host isolation exceptions entry form', () => { await render(); }); - it('should render the form with empty inputs', () => { + // FLAKY: https://github.com/elastic/kibana/issues/140140 + it.skip('should render the form with empty inputs', () => { expect(renderResult.getByTestId('hostIsolationExceptions-form-name-input')).toHaveValue(''); expect(renderResult.getByTestId('hostIsolationExceptions-form-ip-input')).toHaveValue(''); expect( @@ -144,14 +145,16 @@ describe('When on the host isolation exceptions entry form', () => { ).toBe(true); }); - it('should show policy as selected when user clicks on it', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/139776 + it.skip('should show policy as selected when user clicks on it', async () => { userEvent.click(renderResult.getByTestId('perPolicy')); await clickOnEffectedPolicy(renderResult); await expect(isEffectedPolicySelected(renderResult)).resolves.toBe(true); }); - it('should retain the previous policy selection when switching from per-policy to global', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/139899 + it.skip('should retain the previous policy selection when switching from per-policy to global', async () => { // move to per-policy and select the first userEvent.click(renderResult.getByTestId('perPolicy')); await clickOnEffectedPolicy(renderResult); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx index ba830b859d0049..8fb3f683e6eb51 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx @@ -22,8 +22,7 @@ import { getFirstCard } from '../../../components/artifact_list_page/mocks'; jest.mock('../../../../common/components/user_privileges'); const useUserPrivilegesMock = _useUserPrivileges as jest.Mock; -// FLAKY: https://github.com/elastic/kibana/issues/135587 -describe.skip('When on the host isolation exceptions page', () => { +describe('When on the host isolation exceptions page', () => { let render: () => ReturnType<AppContextTestRender['render']>; let renderResult: ReturnType<typeof render>; let history: AppContextTestRender['history']; @@ -78,7 +77,8 @@ describe.skip('When on the host isolation exceptions page', () => { ); }); - it('should hide the Create and Edit actions when host isolation authz is not allowed', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/135587 + it.skip('should hide the Create and Edit actions when host isolation authz is not allowed', async () => { // Use case: license downgrade scenario, where user still has entries defined, but no longer // able to create or edit them (only Delete them) const existingPrivileges = useUserPrivilegesMock(); diff --git a/x-pack/plugins/security_solution/public/management/pages/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/index.test.tsx index 7d2778d602c79e..1df471633c3c22 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.test.tsx @@ -16,8 +16,7 @@ import { endpointPageHttpMock } from './endpoint_hosts/mocks'; jest.mock('../../common/components/user_privileges'); -// FLAKY: https://github.com/elastic/kibana/issues/135166 -describe.skip('when in the Administration tab', () => { +describe('when in the Administration tab', () => { let render: () => ReturnType<AppContextTestRender['render']>; beforeEach(() => { @@ -35,7 +34,8 @@ describe.skip('when in the Administration tab', () => { expect(await render().findByTestId('noIngestPermissions')).not.toBeNull(); }); - it('should display the Management view if user has privileges', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/135166 + it.skip('should display the Management view if user has privileges', async () => { (useUserPrivileges as jest.Mock).mockReturnValue({ endpointPrivileges: { loading: false, canAccessEndpointManagement: true }, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx index b78ad462ae8a17..2a54557b0095b4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx @@ -17,7 +17,7 @@ import { MANAGEMENT_ROUTING_POLICIES_PATH, MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, MANAGEMENT_ROUTING_BLOCKLIST_PATH, - MANAGEMENT_ROUTING_RESPONSE_ACTIONS_PATH, + MANAGEMENT_ROUTING_ACTION_HISTORY_PATH, } from '../common/constants'; import { NotFoundPage } from '../../app/404'; import { EndpointsContainer } from './endpoint_hosts'; @@ -69,9 +69,9 @@ const HostIsolationExceptionsTelemetry = () => ( ); const ResponseActionsTelemetry = () => ( - <TrackApplicationView viewId={SecurityPageName.responseActions}> + <TrackApplicationView viewId={SecurityPageName.actionHistory}> <ResponseActionsContainer /> - <SpyRoute pageName={SecurityPageName.responseActions} /> + <SpyRoute pageName={SecurityPageName.actionHistory} /> </TrackApplicationView> ); @@ -103,7 +103,7 @@ export const ManagementContainer = memo(() => { component={HostIsolationExceptionsTelemetry} /> <Route path={MANAGEMENT_ROUTING_BLOCKLIST_PATH} component={BlocklistContainer} /> - <Route path={MANAGEMENT_ROUTING_RESPONSE_ACTIONS_PATH} component={ResponseActionsTelemetry} /> + <Route path={MANAGEMENT_ROUTING_ACTION_HISTORY_PATH} component={ResponseActionsTelemetry} /> <Route path={MANAGEMENT_PATH} exact> <Redirect to={getEndpointListPath({ name: 'endpointList' })} /> </Route> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_generic_errors_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_generic_errors_list.tsx new file mode 100644 index 00000000000000..cac9c2a7d0c2ce --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_generic_errors_list.tsx @@ -0,0 +1,52 @@ +/* + * 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, useMemo } from 'react'; +import type { PackageGenericErrorsListProps } from '@kbn/fleet-plugin/public'; +import { EuiSpacer } from '@elastic/eui'; + +import { useKibana } from '../../../../../common/lib/kibana'; +import { PackageActionFormatter } from '../../../../components/package_action_item/package_action_formatter'; +import { PackageActionItemError } from '../../../../components/package_action_item/package_action_item_error'; + +/** + * Exports Endpoint-generic errors list + */ +export const EndpointGenericErrorsList = memo<PackageGenericErrorsListProps>( + ({ packageErrors }) => { + const { docLinks } = useKibana().services; + + const globalEndpointErrors = useMemo(() => { + const errors: PackageActionFormatter[] = []; + packageErrors.forEach((unit) => { + if (unit.payload && unit.payload.error) { + errors.push( + new PackageActionFormatter( + unit.payload.error.code, + unit.payload.error.message, + docLinks.links.securitySolution.packageActionTroubleshooting + ) + ); + } + }); + + return errors; + }, [packageErrors, docLinks.links.securitySolution.packageActionTroubleshooting]); + + return ( + <> + {globalEndpointErrors.map((error) => ( + <React.Fragment key={error.key}> + <PackageActionItemError actionFormatter={error} /> + <EuiSpacer size="m" /> + </React.Fragment> + ))} + </> + ); + } +); +EndpointGenericErrorsList.displayName = 'EndpointGenericErrorsList'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_generic_errors_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_generic_errors_list.tsx new file mode 100644 index 00000000000000..1d753bb4f1a262 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_generic_errors_list.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { lazy } from 'react'; +import type { CoreStart } from '@kbn/core/public'; +import type { + PackageGenericErrorsListComponent, + PackageGenericErrorsListProps, +} from '@kbn/fleet-plugin/public'; +import type { StartPlugins } from '../../../../../types'; + +export const getLazyEndpointGenericErrorsListExtension = ( + coreStart: CoreStart, + depsStart: Pick<StartPlugins, 'data' | 'fleet'> +) => { + return lazy<PackageGenericErrorsListComponent>(async () => { + const [{ withSecurityContext }, { EndpointGenericErrorsList }] = await Promise.all([ + import('./with_security_context/with_security_context'), + import('./endpoint_generic_errors_list'), + ]); + + return { + default: withSecurityContext<PackageGenericErrorsListProps>({ + coreStart, + depsStart, + WrappedComponent: EndpointGenericErrorsList, + }), + }; + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx index 659e16dbd01290..5127f0605648ca 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx @@ -24,8 +24,7 @@ const getPackagePolicies = sendGetEndpointSpecificPackagePolicies as jest.Mock; const mockedSendBulkGetAgentPolicies = sendBulkGetAgentPolicyList as jest.Mock; -// FLAKY: https://github.com/elastic/kibana/issues/140153 -describe.skip('When on the policy list page', () => { +describe('When on the policy list page', () => { let render: () => ReturnType<AppContextTestRender['render']>; let renderResult: ReturnType<typeof render>; let history: AppContextTestRender['history']; @@ -119,11 +118,15 @@ describe.skip('When on the policy list page', () => { expect(updatedByCells[0].textContent).toEqual(expectedAvatarName.charAt(0)); expect(firstUpdatedByName.textContent).toEqual(expectedAvatarName); }); - it('should show the correct endpoint count', async () => { + + // FLAKY: https://github.com/elastic/kibana/issues/139778 + it.skip('should show the correct endpoint count', async () => { const endpointCount = renderResult.getAllByTestId('policyEndpointCountLink'); expect(endpointCount[0].textContent).toBe('4'); }); - it('endpoint count link should navigate to the endpoint list filtered by policy', () => { + + // FLAKY: https://github.com/elastic/kibana/issues/140153 + it.skip('endpoint count link should navigate to the endpoint list filtered by policy', () => { const policyId = policies.items[0].id; const filterByPolicyQuery = `?admin_query=(language:kuery,query:'united.endpoint.Endpoint.policy.applied.id : "${policyId}"')`; const backLink = { @@ -186,7 +189,9 @@ describe.skip('When on the policy list page', () => { perPage: 10, }); }); - it('should pass the correct pageSize value to the api', async () => { + + // FLAKY: https://github.com/elastic/kibana/issues/139196 + it.skip('should pass the correct pageSize value to the api', async () => { await waitFor(() => { expect(renderResult.getByTestId('tablePaginationPopoverButton')).toBeTruthy(); }); @@ -206,7 +211,9 @@ describe.skip('When on the policy list page', () => { perPage: 20, }); }); - it('should call the api with the initial pagination values taken from the url', async () => { + + // FLAKY: https://github.com/elastic/kibana/issues/139207 + it.skip('should call the api with the initial pagination values taken from the url', async () => { act(() => { history.push('/administration/policies?page=3&pageSize=50'); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/response_actions/index.tsx b/x-pack/plugins/security_solution/public/management/pages/response_actions/index.tsx index f759830f555fe9..0d3f029cc34cec 100644 --- a/x-pack/plugins/security_solution/public/management/pages/response_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/response_actions/index.tsx @@ -7,7 +7,7 @@ import { Switch } from 'react-router-dom'; import { Route } from '@kbn/kibana-react-plugin/public'; import React, { memo } from 'react'; -import { MANAGEMENT_ROUTING_RESPONSE_ACTIONS_PATH } from '../../common/constants'; +import { MANAGEMENT_ROUTING_ACTION_HISTORY_PATH } from '../../common/constants'; import { NotFoundPage } from '../../../app/404'; import { ResponseActionsListPage } from './view/response_actions_list_page'; @@ -15,7 +15,7 @@ export const ResponseActionsContainer = memo(() => { return ( <Switch> <Route - path={MANAGEMENT_ROUTING_RESPONSE_ACTIONS_PATH} + path={MANAGEMENT_ROUTING_ACTION_HISTORY_PATH} exact component={ResponseActionsListPage} /> diff --git a/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.test.tsx new file mode 100644 index 00000000000000..d22237be1fdf5d --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.test.tsx @@ -0,0 +1,316 @@ +/* + * 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 from 'react'; +import * as reactTestingLibrary from '@testing-library/react'; +import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; +import userEvent from '@testing-library/user-event'; +import { + type AppContextTestRender, + createAppRootMockRenderer, +} from '../../../../common/mock/endpoint'; +import { ResponseActionsListPage } from './response_actions_list_page'; +import type { ActionListApiResponse } from '../../../../../common/endpoint/types'; +import { MANAGEMENT_PATH } from '../../../../../common/constants'; +import { getActionListMock } from '../../../components/endpoint_response_actions_list/mocks'; + +let mockUseGetEndpointActionList: { + isFetched?: boolean; + isFetching?: boolean; + error?: null; + data?: ActionListApiResponse; + refetch: () => unknown; +}; +jest.mock('../../../hooks/endpoint/use_get_endpoint_action_list', () => { + const original = jest.requireActual('../../../hooks/endpoint/use_get_endpoint_action_list'); + return { + ...original, + useGetEndpointActionList: () => mockUseGetEndpointActionList, + }; +}); + +jest.mock('@kbn/kibana-react-plugin/public', () => { + const original = jest.requireActual('@kbn/kibana-react-plugin/public'); + return { + ...original, + useKibana: () => ({ + services: { + uiSettings: { + get: jest.fn().mockImplementation((key) => { + const get = (k: 'dateFormat' | 'timepicker:quickRanges') => { + const x = { + dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', + 'timepicker:quickRanges': [ + { + from: 'now/d', + to: 'now/d', + display: 'Today', + }, + { + from: 'now/w', + to: 'now/w', + display: 'This week', + }, + { + from: 'now-15m', + to: 'now', + display: 'Last 15 minutes', + }, + { + from: 'now-30m', + to: 'now', + display: 'Last 30 minutes', + }, + { + from: 'now-1h', + to: 'now', + display: 'Last 1 hour', + }, + { + from: 'now-24h', + to: 'now', + display: 'Last 24 hours', + }, + { + from: 'now-7d', + to: 'now', + display: 'Last 7 days', + }, + { + from: 'now-30d', + to: 'now', + display: 'Last 30 days', + }, + { + from: 'now-90d', + to: 'now', + display: 'Last 90 days', + }, + { + from: 'now-1y', + to: 'now', + display: 'Last 1 year', + }, + ], + }; + return x[k]; + }; + return get(key); + }), + }, + }, + }), + }; +}); + +describe('Action history page', () => { + const testPrefix = 'response-actions-list'; + + let render: () => ReturnType<AppContextTestRender['render']>; + let renderResult: ReturnType<typeof render>; + let history: AppContextTestRender['history']; + let mockedContext: AppContextTestRender; + + const refetchFunction = jest.fn(); + const baseMockedActionList = { + isFetched: true, + isFetching: false, + error: null, + refetch: refetchFunction, + }; + + beforeEach(async () => { + mockedContext = createAppRootMockRenderer(); + ({ history } = mockedContext); + render = () => (renderResult = mockedContext.render(<ResponseActionsListPage />)); + reactTestingLibrary.act(() => { + history.push(`${MANAGEMENT_PATH}/response_actions`); + }); + + mockUseGetEndpointActionList = { + ...baseMockedActionList, + data: await getActionListMock({ actionCount: 43 }), + }; + }); + + afterEach(() => { + mockUseGetEndpointActionList = { + ...baseMockedActionList, + }; + jest.clearAllMocks(); + }); + + describe('Read from URL params', () => { + it('should read and set paging values from URL params', () => { + reactTestingLibrary.act(() => { + history.push('/administration/action_history?page=3&pageSize=20'); + }); + render(); + const { getByTestId } = renderResult; + + expect(history.location.search).toEqual('?page=3&pageSize=20'); + expect(getByTestId('tablePaginationPopoverButton').textContent).toContain('20'); + expect(getByTestId('pagination-button-2').getAttribute('aria-current')).toStrictEqual('true'); + }); + + it('should read and set command filter values from URL params', () => { + const filterPrefix = 'actions-filter'; + reactTestingLibrary.act(() => { + history.push('/administration/action_history?commands=release,processes'); + }); + + render(); + const { getAllByTestId, getByTestId } = renderResult; + userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`)); + const allFilterOptions = getAllByTestId(`${filterPrefix}-option`); + + const selectedFilterOptions = allFilterOptions.reduce<string[]>((acc, option) => { + if (option.getAttribute('aria-checked') === 'true') { + acc.push(option.textContent?.split('-')[0].trim() as string); + } + return acc; + }, []); + + expect(selectedFilterOptions.length).toEqual(2); + expect(selectedFilterOptions).toEqual(['release', 'processes']); + expect(history.location.search).toEqual('?commands=release,processes'); + }); + + it('should read and set status filter values from URL params', () => { + const filterPrefix = 'statuses-filter'; + reactTestingLibrary.act(() => { + history.push('/administration/action_history?statuses=pending,failed'); + }); + + render(); + const { getAllByTestId, getByTestId } = renderResult; + userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`)); + const allFilterOptions = getAllByTestId(`${filterPrefix}-option`); + + const selectedFilterOptions = allFilterOptions.reduce<string[]>((acc, option) => { + if (option.getAttribute('aria-checked') === 'true') { + acc.push(option.textContent?.split('-')[0].trim() as string); + } + return acc; + }, []); + + expect(selectedFilterOptions.length).toEqual(2); + expect(selectedFilterOptions).toEqual(['Failed', 'Pending']); + expect(history.location.search).toEqual('?statuses=pending,failed'); + }); + + // TODO: add tests for hosts and users when those filters are added + + it('should read and set relative date ranges filter values from URL params', () => { + reactTestingLibrary.act(() => { + history.push('/administration/action_history?startDate=now-23m&endDate=now-1m'); + }); + + render(); + const { getByTestId } = renderResult; + + expect(getByTestId('superDatePickerstartDatePopoverButton').textContent).toEqual( + '~ 23 minutes ago' + ); + expect(getByTestId('superDatePickerendDatePopoverButton').textContent).toEqual( + '~ a minute ago' + ); + expect(history.location.search).toEqual('?startDate=now-23m&endDate=now-1m'); + }); + + it('should read and set absolute date ranges filter values from URL params', () => { + const startDate = '2022-09-12T11:00:00.000Z'; + const endDate = '2022-09-12T11:30:33.000Z'; + reactTestingLibrary.act(() => { + history.push(`/administration/action_history?startDate=${startDate}&endDate=${endDate}`); + }); + + const { getByTestId } = render(); + + expect(getByTestId('superDatePickerstartDatePopoverButton').textContent).toEqual( + 'Sep 12, 2022 @ 07:00:00.000' + ); + expect(getByTestId('superDatePickerendDatePopoverButton').textContent).toEqual( + 'Sep 12, 2022 @ 07:30:33.000' + ); + expect(history.location.search).toEqual(`?startDate=${startDate}&endDate=${endDate}`); + }); + }); + + describe('Set selected/set values to URL params', () => { + it('should set selected page number to URL params', () => { + render(); + const { getByTestId } = renderResult; + + userEvent.click(getByTestId('pagination-button-1')); + expect(history.location.search).toEqual('?page=2&pageSize=10'); + }); + + it('should set selected pageSize value to URL params', () => { + render(); + const { getByTestId } = renderResult; + + userEvent.click(getByTestId('tablePaginationPopoverButton')); + const pageSizeOption = getByTestId('tablePagination-20-rows'); + pageSizeOption.style.pointerEvents = 'all'; + userEvent.click(pageSizeOption); + + expect(history.location.search).toEqual('?page=1&pageSize=20'); + }); + + it('should set selected command filter options to URL params ', () => { + const filterPrefix = 'actions-filter'; + render(); + const { getAllByTestId, getByTestId } = renderResult; + userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`)); + const allFilterOptions = getAllByTestId(`${filterPrefix}-option`); + + allFilterOptions.forEach((option) => { + option.style.pointerEvents = 'all'; + userEvent.click(option); + }); + + expect(history.location.search).toEqual( + '?commands=isolate%2Crelease%2Ckill-process%2Csuspend-process%2Cprocesses' + ); + }); + + it('should set selected status filter options to URL params ', () => { + const filterPrefix = 'statuses-filter'; + render(); + const { getAllByTestId, getByTestId } = renderResult; + userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`)); + const allFilterOptions = getAllByTestId(`${filterPrefix}-option`); + + allFilterOptions.forEach((option) => { + option.style.pointerEvents = 'all'; + userEvent.click(option); + }); + + expect(history.location.search).toEqual('?statuses=failed%2Cpending%2Csuccessful'); + }); + + // TODO: add tests for hosts and users when those filters are added + + it('should set selected relative date range filter options to URL params ', async () => { + const { getByTestId } = render(); + const quickMenuButton = getByTestId('superDatePickerToggleQuickMenuButton'); + const startDatePopoverButton = getByTestId(`superDatePickerShowDatesButton`); + + // shows 24 hours at first + expect(startDatePopoverButton).toHaveTextContent('Last 24 hours'); + + // pick another relative date + userEvent.click(quickMenuButton); + await waitForEuiPopoverOpen(); + userEvent.click(getByTestId('superDatePickerCommonlyUsed_Last_15 minutes')); + expect(startDatePopoverButton).toHaveTextContent('Last 15 minutes'); + + expect(history.location.search).toEqual('?endDate=now&startDate=now-15m'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.tsx b/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.tsx index 044632a3c39848..2b8d21f8f0ac18 100644 --- a/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.tsx @@ -6,14 +6,19 @@ */ import React from 'react'; +import { ACTION_HISTORY } from '../../../../app/translations'; import { AdministrationListPage } from '../../../components/administration_list_page'; import { ResponseActionsLog } from '../../../components/endpoint_response_actions_list/response_actions_log'; import { UX_MESSAGES } from '../../../components/endpoint_response_actions_list/translations'; export const ResponseActionsListPage = () => { return ( - <AdministrationListPage data-test-subj="responseActionsPage" title={UX_MESSAGES.pageTitle}> - <ResponseActionsLog showHostNames={true} /> + <AdministrationListPage + data-test-subj="responseActionsPage" + title={ACTION_HISTORY} + subtitle={UX_MESSAGES.pageSubTitle} + > + <ResponseActionsLog showHostNames={true} isFlyout={false} /> </AdministrationListPage> ); }; diff --git a/x-pack/plugins/security_solution/public/management/types.ts b/x-pack/plugins/security_solution/public/management/types.ts index 2658bd7a58b224..96c1983c8f254e 100644 --- a/x-pack/plugins/security_solution/public/management/types.ts +++ b/x-pack/plugins/security_solution/public/management/types.ts @@ -31,7 +31,7 @@ export enum AdministrationSubTab { eventFilters = 'event_filters', hostIsolationExceptions = 'host_isolation_exceptions', blocklist = 'blocklist', - responseActions = 'response_actions', + actionHistory = 'action_history', } /** diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.test.tsx index b382e6905a60f0..feed7baf8819af 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.test.tsx @@ -84,7 +84,7 @@ describe('RiskScoreDonutChart', () => { expect(mockDispatch).toHaveBeenCalledWith( usersActions.updateTableSorting({ - sort: { field: RiskScoreFields.riskScore, direction: Direction.desc }, + sort: { field: RiskScoreFields.userRiskScore, direction: Direction.desc }, tableType: UsersTableType.risk, }) ); @@ -110,7 +110,7 @@ describe('RiskScoreDonutChart', () => { expect(mockDispatch).toHaveBeenCalledWith( hostsActions.updateHostRiskScoreSort({ - sort: { field: RiskScoreFields.riskScore, direction: Direction.desc }, + sort: { field: RiskScoreFields.hostRiskScore, direction: Direction.desc }, hostsType: HostsType.page, }) ); diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx index dd22104bf39ad1..4ee2bab00f1db0 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx @@ -52,7 +52,7 @@ export const EntityAnalyticsHeader = () => { dispatch( hostsActions.updateHostRiskScoreSort({ - sort: { field: RiskScoreFields.riskScore, direction: Direction.desc }, + sort: { field: RiskScoreFields.hostRiskScore, direction: Direction.desc }, hostsType: HostsType.page, }) ); @@ -74,7 +74,7 @@ export const EntityAnalyticsHeader = () => { dispatch( usersActions.updateTableSorting({ - sort: { field: RiskScoreFields.riskScore, direction: Direction.desc }, + sort: { field: RiskScoreFields.userRiskScore, direction: Direction.desc }, tableType: UsersTableType.risk, }) ); diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/host_risk_score/columns.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/host_risk_score/columns.tsx index affbd9e3357e6c..998a356bf4f738 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/host_risk_score/columns.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/host_risk_score/columns.tsx @@ -12,10 +12,11 @@ import { getEmptyTagValue } from '../../../../common/components/empty_value'; import { HostDetailsLink } from '../../../../common/components/links'; import { HostsTableType } from '../../../../hosts/store/model'; import { RiskScore } from '../../../../common/components/severity/common'; -import type { HostsRiskScore, RiskSeverity } from '../../../../../common/search_strategy'; +import type { HostRiskScore, RiskSeverity } from '../../../../../common/search_strategy'; +import { RiskScoreFields } from '../../../../../common/search_strategy'; import * as i18n from './translations'; -type HostRiskScoreColumns = Array<EuiBasicTableColumn<HostsRiskScore>>; +type HostRiskScoreColumns = Array<EuiBasicTableColumn<HostRiskScore>>; export const getHostRiskScoreColumns = (): HostRiskScoreColumns => [ { @@ -31,7 +32,7 @@ export const getHostRiskScoreColumns = (): HostRiskScoreColumns => [ }, }, { - field: 'risk_stats.risk_score', + field: RiskScoreFields.hostRiskScore, name: i18n.HOST_RISK_SCORE, truncateText: true, mobileOptions: { show: true }, @@ -47,7 +48,7 @@ export const getHostRiskScoreColumns = (): HostRiskScoreColumns => [ }, }, { - field: 'risk', + field: RiskScoreFields.hostRisk, name: ( <EuiToolTip content={i18n.HOST_RISK_TOOLTIP}> <> diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/host_risk_score/index.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/host_risk_score/index.tsx index 9e44561e8b4f5a..fa3cda0921c83a 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/host_risk_score/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/host_risk_score/index.tsx @@ -27,6 +27,7 @@ import { HeaderSection } from '../../../../common/components/header_section'; import { useHostRiskScore, useHostRiskScoreKpi } from '../../../../risk_score/containers'; import type { RiskSeverity } from '../../../../../common/search_strategy'; +import { RiskScoreEntity } from '../../../../../common/search_strategy'; import { SecurityPageName } from '../../../../app/types'; import * as i18n from './translations'; import { generateSeverityFilter } from '../../../../hosts/store/helpers'; @@ -58,7 +59,7 @@ export const EntityAnalyticsHostRiskScores = () => { const riskyHostsFeatureEnabled = useIsExperimentalFeatureEnabled('riskyHostsEnabled'); const severityFilter = useMemo(() => { - const [filter] = generateSeverityFilter(selectedSeverity); + const [filter] = generateSeverityFilter(selectedSeverity, RiskScoreEntity.host); return filter ? JSON.stringify(filter.query) : undefined; }, [selectedSeverity]); @@ -127,7 +128,7 @@ export const EntityAnalyticsHostRiskScores = () => { return null; } - if (!isModuleEnabled) { + if (!isModuleEnabled && !isTableLoading) { return <EntityAnalyticsHostRiskScoresDisable />; } diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/user_risk_score/columns.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/user_risk_score/columns.tsx index c32c2282f367e1..05f532617d5cca 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/user_risk_score/columns.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/user_risk_score/columns.tsx @@ -12,10 +12,11 @@ import { getEmptyTagValue } from '../../../../common/components/empty_value'; import { RiskScore } from '../../../../common/components/severity/common'; import * as i18n from './translations'; import { UsersTableType } from '../../../../users/store/model'; -import type { RiskSeverity, UsersRiskScore } from '../../../../../common/search_strategy'; +import type { RiskSeverity, UserRiskScore } from '../../../../../common/search_strategy'; +import { RiskScoreFields } from '../../../../../common/search_strategy'; import { UserDetailsLink } from '../../../../common/components/links'; -type UserRiskScoreColumns = Array<EuiBasicTableColumn<UsersRiskScore>>; +type UserRiskScoreColumns = Array<EuiBasicTableColumn<UserRiskScore>>; export const getUserRiskScoreColumns = (): UserRiskScoreColumns => [ { @@ -31,7 +32,7 @@ export const getUserRiskScoreColumns = (): UserRiskScoreColumns => [ }, }, { - field: 'risk_stats.risk_score', + field: RiskScoreFields.userRiskScore, name: i18n.USER_RISK_SCORE, truncateText: true, mobileOptions: { show: true }, @@ -47,7 +48,7 @@ export const getUserRiskScoreColumns = (): UserRiskScoreColumns => [ }, }, { - field: 'risk', + field: RiskScoreFields.userRisk, name: ( <EuiToolTip content={i18n.USER_RISK_TOOLTIP}> <> diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/user_risk_score/index.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/user_risk_score/index.tsx index 34c41ee2a00242..68ed1082f4c053 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/user_risk_score/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/user_risk_score/index.tsx @@ -20,6 +20,7 @@ import { LinkButton, useGetSecuritySolutionLinkProps } from '../../../../common/ import { LastUpdatedAt } from '../../detection_response/utils'; import { HeaderSection } from '../../../../common/components/header_section'; import type { RiskSeverity } from '../../../../../common/search_strategy'; +import { RiskScoreEntity } from '../../../../../common/search_strategy'; import { SecurityPageName } from '../../../../app/types'; import * as i18n from './translations'; import { generateSeverityFilter } from '../../../../hosts/store/helpers'; @@ -55,7 +56,7 @@ export const EntityAnalyticsUserRiskScores = () => { const riskyUsersFeatureEnabled = useIsExperimentalFeatureEnabled('riskyUsersEnabled'); const severityFilter = useMemo(() => { - const [filter] = generateSeverityFilter(selectedSeverity); + const [filter] = generateSeverityFilter(selectedSeverity, RiskScoreEntity.user); return filter ? JSON.stringify(filter.query) : undefined; }, [selectedSeverity]); @@ -123,7 +124,7 @@ export const EntityAnalyticsUserRiskScores = () => { return null; } - if (!isModuleEnabled) { + if (!isModuleEnabled && !isTableLoading) { return <EntityAnalyticsUserRiskScoresDisable />; } diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx index 48dea1e5d4b900..721cd5c73f2856 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx @@ -82,11 +82,11 @@ describe('Host Summary Component', () => { { host: { name: 'testHostmame', - }, - risk, - risk_stats: { - rule_risks: [], - risk_score: riskScore, + risk: { + rule_risks: [], + calculated_score_norm: riskScore, + calculated_level: risk, + }, }, }, ], diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx index c6aad526117cd7..d3a1f601445fd0 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx @@ -10,7 +10,7 @@ import { euiLightVars as lightTheme, euiDarkVars as darkTheme } from '@kbn/ui-th import { getOr } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; -import type { HostItem, RiskSeverity } from '../../../../common/search_strategy'; +import type { HostItem } from '../../../../common/search_strategy'; import { buildHostNamesFilter } from '../../../../common/search_strategy'; import { DEFAULT_DARK_MODE } from '../../../../common/constants'; import type { DescriptionList } from '../../../../common/utility_types'; @@ -108,7 +108,9 @@ export const HostOverview = React.memo<HostSummaryProps>( title: i18n.HOST_RISK_SCORE, description: ( <> - {hostRiskData ? Math.round(hostRiskData.risk_stats.risk_score) : getEmptyTagValue()} + {hostRiskData + ? Math.round(hostRiskData.host.risk.calculated_score_norm) + : getEmptyTagValue()} </> ), }, @@ -118,7 +120,10 @@ export const HostOverview = React.memo<HostSummaryProps>( description: ( <> {hostRiskData ? ( - <RiskScore severity={hostRiskData.risk as RiskSeverity} hideBackgroundColor /> + <RiskScore + severity={hostRiskData.host.risk.calculated_level} + hideBackgroundColor + /> ) : ( getEmptyTagValue() )} diff --git a/x-pack/plugins/security_solution/public/overview/components/link_panel/inner_link_panel.tsx b/x-pack/plugins/security_solution/public/overview/components/link_panel/inner_link_panel.tsx index c4f234b43efd0f..f76b446ac72e81 100644 --- a/x-pack/plugins/security_solution/public/overview/components/link_panel/inner_link_panel.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/link_panel/inner_link_panel.tsx @@ -8,7 +8,7 @@ import React from 'react'; import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiSplitPanel, EuiText } from '@elastic/eui'; -import { LEARN_MORE } from '../overview_risky_host_links/translations'; +import * as i18n from './translations'; const ButtonContainer = styled(EuiFlexGroup)` padding: ${({ theme }) => theme.eui.euiSizeS}; @@ -66,7 +66,7 @@ export const InnerLinkPanel = ({ data-test-subj={`${dataTestSubj}-learn-more`} external > - {LEARN_MORE} + {i18n.LEARN_MORE} </EuiLink> )} </p> diff --git a/x-pack/plugins/cases/public/components/user_list/translations.ts b/x-pack/plugins/security_solution/public/overview/components/link_panel/translations.ts similarity index 59% rename from x-pack/plugins/cases/public/components/user_list/translations.ts rename to x-pack/plugins/security_solution/public/overview/components/link_panel/translations.ts index 73610e59593450..edbfa06477ba5a 100644 --- a/x-pack/plugins/cases/public/components/user_list/translations.ts +++ b/x-pack/plugins/security_solution/public/overview/components/link_panel/translations.ts @@ -7,8 +7,9 @@ import { i18n } from '@kbn/i18n'; -export const SEND_EMAIL_ARIA = (user: string) => - i18n.translate('xpack.cases.caseView.sendEmalLinkAria', { - values: { user }, - defaultMessage: 'click to send an email to {user}', - }); +export const LEARN_MORE = i18n.translate( + 'xpack.securitySolution.overview.linkPanelLearnMoreButton', + { + defaultMessage: 'Learn More', + } +); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx index 371f9a1e79f20e..c6a623f19681fb 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx @@ -16,7 +16,6 @@ import type { LinkPanelViewProps } from '../link_panel/types'; import { shortenCountIntoString } from '../../../common/utils/shorten_count_into_string'; import { Link } from '../link_panel/link'; import { ID as CTIEventCountQueryId } from '../../containers/overview_cti_links/use_ti_data_sources'; -import { LINK_COPY } from '../overview_risky_host_links/translations'; const columns: Array<EuiTableFieldDataColumnType<LinkPanelListItem>> = [ { name: 'Name', field: 'title', sortable: true, truncateText: true, width: '100%' }, @@ -34,7 +33,7 @@ const columns: Array<EuiTableFieldDataColumnType<LinkPanelListItem>> = [ field: 'path', truncateText: true, width: '80px', - render: (path: string) => <Link path={path} copy={LINK_COPY} />, + render: (path: string) => <Link path={path} copy={i18n.LINK_COPY} />, }, ]; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts index 775dab6721da12..ef7f1f6540ee50 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts @@ -42,3 +42,7 @@ export const OTHER_DATA_SOURCE_TITLE = i18n.translate( defaultMessage: 'Others', } ); + +export const LINK_COPY = i18n.translate('xpack.securitySolution.overview.ctiLinkSource', { + defaultMessage: 'Source', +}); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx index 6e35d801c75d9d..c985d5a7af6552 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx @@ -11,18 +11,17 @@ import numeral from '@elastic/numeral'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { useMemo, useCallback, useState, useEffect } from 'react'; -import { DEFAULT_NUMBER_FORMAT, APP_UI_ID } from '../../../../common/constants'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; import type { ESQuery } from '../../../../common/typed_json'; import { ID as OverviewHostQueryId, useHostOverview } from '../../containers/overview_host'; import { HeaderSection } from '../../../common/components/header_section'; -import { useUiSetting$, useKibana } from '../../../common/lib/kibana'; -import { getHostDetailsUrl, useFormatUrl } from '../../../common/components/link_to'; +import { useUiSetting$ } from '../../../common/lib/kibana'; import { getOverviewHostStats, OverviewHostStats } from '../overview_host_stats'; import { manageQuery } from '../../../common/components/page/manage_query'; import { InspectButtonContainer } from '../../../common/components/inspect'; +import { SecuritySolutionLinkButton } from '../../../common/components/links'; import type { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { SecurityPageName } from '../../../app/types'; -import { LinkButton } from '../../../common/components/links'; import { useQueryToggle } from '../../../common/containers/query_toggle'; export interface OwnProps { @@ -43,8 +42,6 @@ const OverviewHostComponent: React.FC<OverviewHostProps> = ({ startDate, setQuery, }) => { - const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.hosts); - const { navigateToApp } = useKibana().services.application; const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); const { toggleStatus, setToggleStatus } = useQueryToggle(OverviewHostQueryId); @@ -69,17 +66,6 @@ const OverviewHostComponent: React.FC<OverviewHostProps> = ({ skip: querySkip, }); - const goToHost = useCallback( - (ev) => { - ev.preventDefault(); - navigateToApp(APP_UI_ID, { - deepLinkId: SecurityPageName.hosts, - path: getHostDetailsUrl('allHosts', urlSearch), - }); - }, - [navigateToApp, urlSearch] - ); - const hostEventsCount = useMemo( () => getOverviewHostStats(overviewHost).reduce((total, stat) => total + stat.count, 0), [overviewHost] @@ -90,18 +76,6 @@ const OverviewHostComponent: React.FC<OverviewHostProps> = ({ [defaultNumberFormat, hostEventsCount] ); - const hostPageButton = useMemo( - () => ( - <LinkButton onClick={goToHost} href={formatUrl('/allHosts')}> - <FormattedMessage - id="xpack.securitySolution.overview.hostsAction" - defaultMessage="View hosts" - /> - </LinkButton> - ), - [goToHost, formatUrl] - ); - const title = useMemo( () => ( <FormattedMessage @@ -140,7 +114,12 @@ const OverviewHostComponent: React.FC<OverviewHostProps> = ({ title={title} isInspectDisabled={filterQuery === undefined} > - <>{hostPageButton}</> + <SecuritySolutionLinkButton deepLinkId={SecurityPageName.hosts}> + <FormattedMessage + id="xpack.securitySolution.overview.hostsAction" + defaultMessage="View hosts" + /> + </SecuritySolutionLinkButton> </HeaderSection> {toggleStatus && ( <OverviewHostStatsManage diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.test.tsx deleted file mode 100644 index b0c5f8bc7cff96..00000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.test.tsx +++ /dev/null @@ -1,112 +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 React from 'react'; -import { Provider } from 'react-redux'; -import { cloneDeep } from 'lodash/fp'; -import { render, screen } from '@testing-library/react'; -import { I18nProvider } from '@kbn/i18n-react'; -import { ThemeProvider } from 'styled-components'; -import { mockTheme } from '../overview_cti_links/mock'; -import { RiskyHostLinks } from '.'; -import type { State } from '../../../common/store'; -import { createStore } from '../../../common/store'; -import { - createSecuritySolutionStorageMock, - kibanaObservable, - mockGlobalState, - SUB_PLUGINS_REDUCER, -} from '../../../common/mock'; - -import { useRiskyHostsDashboardLinks } from '../../containers/overview_risky_host_links/use_risky_hosts_dashboard_links'; -import { useHostRiskScore } from '../../../risk_score/containers'; -import { useDashboardButtonHref } from '../../../common/hooks/use_dashboard_button_href'; - -jest.mock('../../../common/lib/kibana'); - -jest.mock('../../../risk_score/containers'); -const useHostRiskScoreMock = useHostRiskScore as jest.Mock; - -jest.mock('../../../common/hooks/use_dashboard_button_href'); -const useRiskyHostsDashboardButtonHrefMock = useDashboardButtonHref as jest.Mock; -useRiskyHostsDashboardButtonHrefMock.mockReturnValue({ buttonHref: '/test' }); - -jest.mock('../../containers/overview_risky_host_links/use_risky_hosts_dashboard_links'); -const useRiskyHostsDashboardLinksMock = useRiskyHostsDashboardLinks as jest.Mock; -useRiskyHostsDashboardLinksMock.mockReturnValue({ - listItemsWithLinks: [{ title: 'a', count: 1, path: '/test' }], -}); - -describe('RiskyHostLinks', () => { - const state: State = mockGlobalState; - - const { storage } = createSecuritySolutionStorageMock(); - let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - - beforeEach(() => { - const myState = cloneDeep(state); - store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - }); - - it('renders enabled module view if module is enabled', () => { - useHostRiskScoreMock.mockReturnValueOnce([ - false, - { - data: [], - isModuleEnabled: true, - }, - ]); - - render( - <Provider store={store}> - <I18nProvider> - <ThemeProvider theme={mockTheme}> - <RiskyHostLinks - setQuery={jest.fn()} - deleteQuery={jest.fn()} - timerange={{ - to: 'now', - from: 'now-30d', - }} - /> - </ThemeProvider> - </I18nProvider> - </Provider> - ); - - expect(screen.queryByTestId('risky-hosts-enable-module-button')).not.toBeInTheDocument(); - }); - - it('renders disabled module view if module is disabled', () => { - useHostRiskScoreMock.mockReturnValueOnce([ - false, - { - data: [], - isModuleEnabled: false, - }, - ]); - - render( - <Provider store={store}> - <I18nProvider> - <ThemeProvider theme={mockTheme}> - <RiskyHostLinks - setQuery={jest.fn()} - deleteQuery={jest.fn()} - timerange={{ - to: 'now', - from: 'now-30d', - }} - /> - </ThemeProvider> - </I18nProvider> - </Provider> - ); - - expect(screen.getByTestId('disabled-open-in-console-button-with-tooltip')).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.tsx deleted file mode 100644 index df6286647e82ee..00000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.tsx +++ /dev/null @@ -1,53 +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 React from 'react'; - -import { RiskyHostsEnabledModule } from './risky_hosts_enabled_module'; -import { RiskyHostsDisabledModule } from './risky_hosts_disabled_module'; -import { useQueryInspector } from '../../../common/components/page/manage_query'; -import type { GlobalTimeArgs } from '../../../common/containers/use_global_time'; -import { useHostRiskScore, HostRiskScoreQueryId } from '../../../risk_score/containers'; -export interface RiskyHostLinksProps extends Pick<GlobalTimeArgs, 'deleteQuery' | 'setQuery'> { - timerange: { to: string; from: string }; -} - -const QUERY_ID = HostRiskScoreQueryId.OVERVIEW_RISKY_HOSTS; - -const RiskyHostLinksComponent: React.FC<RiskyHostLinksProps> = ({ - timerange, - deleteQuery, - setQuery, -}) => { - const [loading, { data, isModuleEnabled, inspect, refetch }] = useHostRiskScore({ - timerange, - }); - - useQueryInspector({ - queryId: QUERY_ID, - loading, - refetch, - setQuery, - deleteQuery, - inspect, - }); - - switch (isModuleEnabled) { - case true: - return ( - <RiskyHostsEnabledModule to={timerange.to} from={timerange.from} hostRiskScore={data} /> - ); - case false: - return <RiskyHostsDisabledModule />; - case undefined: - default: - return null; - } -}; - -export const RiskyHostLinks = React.memo(RiskyHostLinksComponent); -RiskyHostLinks.displayName = 'RiskyHostLinks'; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/navigate_to_host.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/navigate_to_host.tsx deleted file mode 100644 index afa0cfe7e9ae8c..00000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/navigate_to_host.tsx +++ /dev/null @@ -1,43 +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 React, { useCallback } from 'react'; -import { EuiButtonEmpty, EuiText, EuiToolTip } from '@elastic/eui'; -import { APP_UI_ID, SecurityPageName } from '../../../../common/constants'; -import { useKibana } from '../../../common/lib/kibana'; - -export const NavigateToHost: React.FC<{ name: string }> = ({ name }): JSX.Element => { - const { navigateToApp } = useKibana().services.application; - const { filterManager } = useKibana().services.data.query; - - const goToHostPage = useCallback( - (e) => { - e.preventDefault(); - filterManager.addFilters([ - { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { match_phrase: { 'host.name': name } }, - }, - ]); - navigateToApp(APP_UI_ID, { - deepLinkId: SecurityPageName.hosts, - }); - }, - [filterManager, name, navigateToApp] - ); - return ( - <EuiToolTip content={name} position="top"> - <EuiButtonEmpty color="text" onClick={goToHostPage} size="xs"> - <EuiText size="s">{name}</EuiText> - </EuiButtonEmpty> - </EuiToolTip> - ); -}; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_disabled_module.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_disabled_module.test.tsx deleted file mode 100644 index e8a50c83a3a273..00000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_disabled_module.test.tsx +++ /dev/null @@ -1,54 +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 React from 'react'; -import { Provider } from 'react-redux'; -import { cloneDeep } from 'lodash/fp'; -import { render, screen } from '@testing-library/react'; -import { I18nProvider } from '@kbn/i18n-react'; -import { ThemeProvider } from 'styled-components'; -import type { State } from '../../../common/store'; -import { createStore } from '../../../common/store'; -import { - createSecuritySolutionStorageMock, - kibanaObservable, - mockGlobalState, - SUB_PLUGINS_REDUCER, -} from '../../../common/mock'; -import { RiskyHostsDisabledModule } from './risky_hosts_disabled_module'; -import { mockTheme } from '../overview_cti_links/mock'; - -jest.mock('../../../common/lib/kibana'); - -describe('RiskyHostsModule', () => { - const state: State = mockGlobalState; - - const { storage } = createSecuritySolutionStorageMock(); - let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - - beforeEach(() => { - const myState = cloneDeep(state); - store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - }); - - it('renders expected children', () => { - render( - <Provider store={store}> - <I18nProvider> - <ThemeProvider theme={mockTheme}> - <RiskyHostsDisabledModule /> - </ThemeProvider> - </I18nProvider> - </Provider> - ); - - expect(screen.getByTestId('risky-hosts-dashboard-links')).toBeInTheDocument(); - expect(screen.getByTestId('risky-hosts-inner-panel-danger-learn-more')).toBeInTheDocument(); - - expect(screen.getByTestId('disabled-open-in-console-button-with-tooltip')).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_disabled_module.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_disabled_module.tsx deleted file mode 100644 index e13089dc6404ed..00000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_disabled_module.tsx +++ /dev/null @@ -1,51 +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 React from 'react'; - -import * as i18n from './translations'; -import { DisabledLinkPanel } from '../link_panel/disabled_link_panel'; -import { RiskyHostsPanelView } from './risky_hosts_panel_view'; - -import { ENABLE_VIA_DEV_TOOLS } from './translations'; - -import { OpenInDevConsoleButton } from '../../../common/components/open_in_dev_console'; -import { useCheckSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_check_signal_index'; -import type { LinkPanelListItem } from '../link_panel'; -import { useEnableHostRiskFromUrl } from '../../../common/hooks/use_enable_host_risk_from_url'; - -export const RISKY_HOSTS_DOC_LINK = - 'https://www.github.com/elastic/detection-rules/blob/main/docs/experimental-machine-learning/host-risk-score.md'; - -const emptyList: LinkPanelListItem[] = []; - -export const RiskyHostsDisabledModuleComponent = () => { - const loadFromUrl = useEnableHostRiskFromUrl(); - const { signalIndexExists } = useCheckSignalIndex(); - - return ( - <DisabledLinkPanel - bodyCopy={i18n.DANGER_BODY} - dataTestSubjPrefix="risky-hosts" - learnMoreUrl={RISKY_HOSTS_DOC_LINK} - listItems={emptyList} - titleCopy={i18n.DANGER_TITLE} - LinkPanelViewComponent={RiskyHostsPanelView} - moreButtons={ - <OpenInDevConsoleButton - href={loadFromUrl} - enableButton={!!signalIndexExists} - title={ENABLE_VIA_DEV_TOOLS} - tooltipContent={i18n.ENABLE_RISK_SCORE_POPOVER} - /> - } - /> - ); -}; - -export const RiskyHostsDisabledModule = React.memo(RiskyHostsDisabledModuleComponent); -RiskyHostsDisabledModule.displayName = 'RiskyHostsDisabledModule'; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.test.tsx deleted file mode 100644 index d5e77c478aa1d8..00000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.test.tsx +++ /dev/null @@ -1,80 +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 React from 'react'; -import { Provider } from 'react-redux'; -import { cloneDeep } from 'lodash/fp'; -import { render, screen } from '@testing-library/react'; -import { I18nProvider } from '@kbn/i18n-react'; -import { ThemeProvider } from 'styled-components'; -import type { State } from '../../../common/store'; -import { createStore } from '../../../common/store'; -import { - createSecuritySolutionStorageMock, - kibanaObservable, - mockGlobalState, - SUB_PLUGINS_REDUCER, -} from '../../../common/mock'; - -import { useRiskyHostsDashboardLinks } from '../../containers/overview_risky_host_links/use_risky_hosts_dashboard_links'; -import { mockTheme } from '../overview_cti_links/mock'; -import { RiskyHostsEnabledModule } from './risky_hosts_enabled_module'; -import { useDashboardButtonHref } from '../../../common/hooks/use_dashboard_button_href'; - -jest.mock('../../../common/lib/kibana'); - -jest.mock('../../../common/hooks/use_dashboard_button_href'); -const useRiskyHostsDashboardButtonHrefMock = useDashboardButtonHref as jest.Mock; -useRiskyHostsDashboardButtonHrefMock.mockReturnValue({ buttonHref: '/test' }); - -jest.mock('../../containers/overview_risky_host_links/use_risky_hosts_dashboard_links'); -const useRiskyHostsDashboardLinksMock = useRiskyHostsDashboardLinks as jest.Mock; -useRiskyHostsDashboardLinksMock.mockReturnValue({ - listItemsWithLinks: [{ title: 'a', count: 1, path: '/test' }], -}); - -describe('RiskyHostsEnabledModule', () => { - const state: State = mockGlobalState; - - const { storage } = createSecuritySolutionStorageMock(); - let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - - beforeEach(() => { - const myState = cloneDeep(state); - store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - }); - - it('renders expected children', () => { - render( - <Provider store={store}> - <I18nProvider> - <ThemeProvider theme={mockTheme}> - <RiskyHostsEnabledModule - hostRiskScore={[ - { - '@timestamp': '1641902481', - host: { - name: 'a', - }, - risk_stats: { - risk_score: 1, - rule_risks: [], - }, - risk: '', - }, - ]} - to={'now'} - from={'now-30d'} - /> - </ThemeProvider> - </I18nProvider> - </Provider> - ); - expect(screen.getByTestId('risky-hosts-dashboard-links')).toBeInTheDocument(); - expect(screen.getByTestId('create-saved-object-success-button')).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.tsx deleted file mode 100644 index fae3c4db217376..00000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.tsx +++ /dev/null @@ -1,43 +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 React, { useMemo } from 'react'; -import { RiskyHostsPanelView } from './risky_hosts_panel_view'; -import type { LinkPanelListItem } from '../link_panel'; -import { useRiskyHostsDashboardLinks } from '../../containers/overview_risky_host_links/use_risky_hosts_dashboard_links'; -import type { HostsRiskScore } from '../../../../common/search_strategy'; - -const getListItemsFromHits = (items: HostsRiskScore[]): LinkPanelListItem[] => { - return items.map(({ host, risk_stats: riskStats, risk: copy }) => ({ - title: host.name, - count: riskStats.risk_score, - copy, - path: '', - })); -}; - -const RiskyHostsEnabledModuleComponent: React.FC<{ - from: string; - hostRiskScore?: HostsRiskScore[]; - to: string; -}> = ({ hostRiskScore, to, from }) => { - const listItems = useMemo(() => getListItemsFromHits(hostRiskScore || []), [hostRiskScore]); - const { listItemsWithLinks } = useRiskyHostsDashboardLinks(to, from, listItems); - - return ( - <RiskyHostsPanelView - isInspectEnabled - listItems={listItemsWithLinks} - totalCount={listItems.length} - to={to} - from={from} - /> - ); -}; - -export const RiskyHostsEnabledModule = React.memo(RiskyHostsEnabledModuleComponent); -RiskyHostsEnabledModule.displayName = 'RiskyHostsEnabledModule'; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.test.tsx deleted file mode 100644 index 863bd4fcbd35d9..00000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.test.tsx +++ /dev/null @@ -1,67 +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 React from 'react'; -import { render, screen } from '@testing-library/react'; -import type { State } from '../../../common/store'; -import { createStore } from '../../../common/store'; -import { - createSecuritySolutionStorageMock, - kibanaObservable, - mockGlobalState, - SUB_PLUGINS_REDUCER, - TestProviders, -} from '../../../common/mock'; - -import { RiskyHostsPanelView } from './risky_hosts_panel_view'; -import { useDashboardButtonHref } from '../../../common/hooks/use_dashboard_button_href'; - -jest.mock('../../../common/lib/kibana'); - -jest.mock('../../../common/hooks/use_dashboard_button_href'); -const useRiskyHostsDashboardButtonHrefMock = useDashboardButtonHref as jest.Mock; -useRiskyHostsDashboardButtonHrefMock.mockReturnValue({ buttonHref: '/test' }); - -describe('RiskyHostsPanelView', () => { - const state: State = mockGlobalState; - - beforeEach(() => { - const { storage } = createSecuritySolutionStorageMock(); - const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - render( - <TestProviders store={store}> - <RiskyHostsPanelView - isInspectEnabled={true} - listItems={[{ title: 'a', count: 1, path: '/test' }]} - totalCount={1} - to="now" - from="now-30d" - /> - </TestProviders> - ); - }); - - it('renders title', () => { - expect(screen.getByTestId('header-section-title')).toHaveTextContent( - 'Current host risk scores' - ); - }); - - it('renders host number', () => { - expect(screen.getByTestId('header-panel-subtitle')).toHaveTextContent('Showing: 1 host'); - }); - - it('renders view dashboard button', () => { - expect(screen.getByTestId('create-saved-object-success-button')).toHaveAttribute( - 'href', - '/test' - ); - expect(screen.getByTestId('create-saved-object-success-button')).toHaveTextContent( - 'View dashboard' - ); - }); -}); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx deleted file mode 100644 index 7aadf6bcfa991c..00000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx +++ /dev/null @@ -1,157 +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 React, { useCallback, useMemo, useState } from 'react'; - -import type { EuiTableFieldDataColumnType } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import type { SavedObject, SavedObjectAttributes } from '@kbn/core/types'; -import type { LinkPanelListItem } from '../link_panel'; -import { InnerLinkPanel, LinkPanel } from '../link_panel'; -import type { LinkPanelViewProps } from '../link_panel/types'; -import { Link } from '../link_panel/link'; -import * as i18n from './translations'; -import { NavigateToHost } from './navigate_to_host'; -import { HostRiskScoreQueryId } from '../../../risk_score/containers'; -import { useKibana } from '../../../common/lib/kibana'; -import { RISKY_HOSTS_DASHBOARD_TITLE } from '../../../hosts/pages/navigation/constants'; -import { useDashboardButtonHref } from '../../../common/hooks/use_dashboard_button_href'; -import { ImportSavedObjectsButton } from '../../../common/components/create_prebuilt_saved_objects/components/bulk_create_button'; -import { VIEW_DASHBOARD } from '../overview_cti_links/translations'; - -const columns: Array<EuiTableFieldDataColumnType<LinkPanelListItem>> = [ - { - name: 'Host Name', - field: 'title', - sortable: true, - truncateText: true, - width: '55%', - render: (name) => (<NavigateToHost name={name} />) as JSX.Element, - }, - { - align: 'right', - field: 'count', - name: 'Risk Score', - render: (riskScore) => - Number.isNaN(riskScore) ? riskScore : Number.parseFloat(riskScore).toFixed(2), - sortable: true, - truncateText: true, - width: '15%', - }, - { - field: 'copy', - name: 'Current Risk', - sortable: true, - truncateText: true, - width: '15%', - }, - { - field: 'path', - name: '', - render: (path: string) => (<Link path={path} copy={i18n.LINK_COPY} />) as JSX.Element, - truncateText: true, - width: '80px', - }, -]; - -const warningPanel = ( - <InnerLinkPanel - color={'warning'} - title={i18n.WARNING_TITLE} - body={i18n.WARNING_BODY} - dataTestSubj="risky-hosts-inner-panel-warning" - /> -); - -const RiskyHostsPanelViewComponent: React.FC<LinkPanelViewProps> = ({ - isInspectEnabled, - listItems, - splitPanel, - totalCount = 0, - to, - from, -}) => { - const splitPanelElement = - typeof splitPanel === 'undefined' - ? listItems.length === 0 - ? warningPanel - : undefined - : splitPanel; - - const [dashboardUrl, setDashboardUrl] = useState<string>(); - const { buttonHref } = useDashboardButtonHref({ - to, - from, - title: RISKY_HOSTS_DASHBOARD_TITLE, - }); - const { - services: { dashboard }, - } = useKibana(); - - const onImportDashboardSuccessCallback = useCallback( - (response: Array<SavedObject<SavedObjectAttributes>>) => { - const targetDashboard = response.find( - (obj) => obj.type === 'dashboard' && obj?.attributes?.title === RISKY_HOSTS_DASHBOARD_TITLE - ); - - const fetchDashboardUrl = (targetDashboardId: string | null | undefined) => { - if (to && from && targetDashboardId) { - const targetUrl = dashboard?.locator?.getRedirectUrl({ - dashboardId: targetDashboardId, - timeRange: { - to, - from, - }, - }); - - setDashboardUrl(targetUrl); - } - }; - - fetchDashboardUrl(targetDashboard?.id); - }, - [dashboard?.locator, from, to] - ); - - return ( - <LinkPanel - {...{ - button: ( - <ImportSavedObjectsButton - hide={listItems == null || listItems.length === 0} - onSuccessCallback={onImportDashboardSuccessCallback} - successLink={buttonHref || dashboardUrl} - successTitle={VIEW_DASHBOARD} - templateName="hostRiskScoreDashboards" - title={i18n.IMPORT_DASHBOARD} - /> - ), - columns, - dataTestSubj: 'risky-hosts-dashboard-links', - defaultSortField: 'count', - defaultSortOrder: 'desc', - inspectQueryId: isInspectEnabled ? HostRiskScoreQueryId.OVERVIEW_RISKY_HOSTS : undefined, - listItems, - panelTitle: i18n.PANEL_TITLE, - splitPanel: splitPanelElement, - subtitle: useMemo( - () => ( - <FormattedMessage - data-test-subj="risky-hosts-total-event-count" - defaultMessage="Showing: {totalCount} {totalCount, plural, one {host} other {hosts}}" - id="xpack.securitySolution.overview.riskyHostsDashboardSubtitle" - values={{ totalCount }} - /> - ), - [totalCount] - ), - }} - /> - ); -}; - -export const RiskyHostsPanelView = React.memo(RiskyHostsPanelViewComponent); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/translations.ts b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/translations.ts deleted file mode 100644 index 5ba4bb2323b24b..00000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/translations.ts +++ /dev/null @@ -1,72 +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 { i18n } from '@kbn/i18n'; - -export const WARNING_TITLE = i18n.translate( - 'xpack.securitySolution.overview.riskyHostsDashboardWarningPanelTitle', - { - defaultMessage: 'No host risk score data available to display', - } -); - -export const WARNING_BODY = i18n.translate( - 'xpack.securitySolution.overview.riskyHostsDashboardWarningPanelBody', - { - defaultMessage: `We haven't detected any host risk score data from the hosts in your environment for the selected time range.`, - } -); - -export const DANGER_TITLE = i18n.translate( - 'xpack.securitySolution.overview.riskyHostsDashboardDangerPanelTitle', - { - defaultMessage: 'No host risk score data', - } -); - -export const DANGER_BODY = i18n.translate( - 'xpack.securitySolution.overview.riskyHostsDashboardEnableThreatIntel', - { - defaultMessage: 'You must enable the host risk module to view risky hosts.', - } -); - -export const ENABLE_VIA_DEV_TOOLS = i18n.translate( - 'xpack.securitySolution.overview.riskyHostsDashboardDangerPanelButton', - { - defaultMessage: 'Enable via Dev Tools', - } -); - -export const LEARN_MORE = i18n.translate( - 'xpack.securitySolution.overview.riskyHostsDashboardLearnMoreButton', - { - defaultMessage: 'Learn More', - } -); - -export const LINK_COPY = i18n.translate('xpack.securitySolution.overview.riskyHostsSource', { - defaultMessage: 'Source', -}); - -export const PANEL_TITLE = i18n.translate( - 'xpack.securitySolution.overview.riskyHostsDashboardTitle', - { - defaultMessage: 'Current host risk scores', - } -); - -export const IMPORT_DASHBOARD = i18n.translate('xpack.securitySolution.overview.importDasboard', { - defaultMessage: 'Import dashboard', -}); - -export const ENABLE_RISK_SCORE_POPOVER = i18n.translate( - 'xpack.securitySolution.overview.enableRiskScorePopoverTitle', - { - defaultMessage: 'Alerts need to be available before enabling module', - } -); diff --git a/x-pack/plugins/security_solution/public/overview/components/user_overview/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/user_overview/index.test.tsx index 5cf51615a395d6..9bc5ce903e2caf 100644 --- a/x-pack/plugins/security_solution/public/overview/components/user_overview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/user_overview/index.test.tsx @@ -93,13 +93,13 @@ describe('User Summary Component', () => { { data: [ { - host: { + user: { name: 'testUsermame', - }, - risk, - risk_stats: { - rule_risks: [], - risk_score: riskScore, + risk: { + rule_risks: [], + calculated_level: risk, + calculated_score_norm: riskScore, + }, }, }, ], diff --git a/x-pack/plugins/security_solution/public/overview/components/user_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/user_overview/index.tsx index 6349a33a58fa33..6c5f4a952e9a71 100644 --- a/x-pack/plugins/security_solution/public/overview/components/user_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/user_overview/index.tsx @@ -106,7 +106,9 @@ export const UserOverview = React.memo<UserSummaryProps>( title: i18n.USER_RISK_SCORE, description: ( <> - {userRiskData ? Math.round(userRiskData.risk_stats.risk_score) : getEmptyTagValue()} + {userRiskData + ? Math.round(userRiskData.user.risk.calculated_score_norm) + : getEmptyTagValue()} </> ), }, @@ -115,7 +117,10 @@ export const UserOverview = React.memo<UserSummaryProps>( description: ( <> {userRiskData ? ( - <RiskScore severity={userRiskData.risk as RiskSeverity} hideBackgroundColor /> + <RiskScore + severity={userRiskData.user.risk.calculated_level as RiskSeverity} + hideBackgroundColor + /> ) : ( getEmptyTagValue() )} diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_id.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_id.ts deleted file mode 100644 index 1e0758343ba474..00000000000000 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_id.ts +++ /dev/null @@ -1,41 +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 { useState, useEffect } from 'react'; -import type { SavedObjectAttributes } from '@kbn/securitysolution-io-ts-alerting-types'; -import { useKibana } from '../../../common/lib/kibana'; - -const DASHBOARD_REQUEST_BODY_SEARCH = '"Drilldown of Host Risk Score"'; -export const DASHBOARD_REQUEST_BODY = { - type: 'dashboard', - search: DASHBOARD_REQUEST_BODY_SEARCH, - fields: ['title'], -}; - -export const useRiskyHostsDashboardId = () => { - const savedObjectsClient = useKibana().services.savedObjects.client; - const [dashboardId, setDashboardId] = useState<string | undefined>(); - - useEffect(() => { - if (savedObjectsClient) { - savedObjectsClient.find<SavedObjectAttributes>(DASHBOARD_REQUEST_BODY).then( - async (DashboardsSO?: { - savedObjects?: Array<{ - attributes?: SavedObjectAttributes; - id?: string; - }>; - }) => { - if (DashboardsSO?.savedObjects?.length) { - setDashboardId(DashboardsSO.savedObjects[0].id); - } - } - ); - } - }, [savedObjectsClient]); - - return dashboardId; -}; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx deleted file mode 100644 index bf09bb56bb6f41..00000000000000 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx +++ /dev/null @@ -1,73 +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 { useState, useEffect } from 'react'; -import { useKibana } from '../../../common/lib/kibana'; -import type { LinkPanelListItem } from '../../components/link_panel'; -import { useRiskyHostsDashboardId } from './use_risky_hosts_dashboard_id'; - -export const useRiskyHostsDashboardLinks = ( - to: string, - from: string, - listItems: LinkPanelListItem[] -) => { - const { dashboard } = useKibana().services; - - const dashboardId = useRiskyHostsDashboardId(); - const [listItemsWithLinks, setListItemsWithLinks] = useState<LinkPanelListItem[]>([]); - - useEffect(() => { - let cancelled = false; - const createLinks = async () => { - if (dashboard?.locator && dashboardId) { - const dashboardUrls = await Promise.all( - listItems.reduce( - (acc: Array<Promise<string>>, listItem) => - dashboard && dashboard.locator - ? [ - ...acc, - dashboard.locator.getUrl({ - dashboardId, - timeRange: { - to, - from, - }, - filters: [ - { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { match_phrase: { 'host.name': listItem.title } }, - }, - ], - }), - ] - : acc, - [] - ) - ); - if (!cancelled && dashboardUrls.length) { - setListItemsWithLinks( - listItems.map((item, i) => ({ - ...item, - path: dashboardUrls[i], - })) - ); - } - } else { - setListItemsWithLinks(listItems); - } - }; - createLinks(); - return () => { - cancelled = true; - }; - }, [dashboard, dashboardId, from, listItems, to]); - - return { listItemsWithLinks }; -}; diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 2e3aa7c4d8d282..6cccf353e4b1c5 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -30,9 +30,7 @@ import { useDeepEqualSelector } from '../../common/hooks/use_selector'; import { ThreatIntelLinkPanel } from '../components/overview_cti_links'; import { useAllTiDataSources } from '../containers/overview_cti_links/use_all_ti_data_sources'; import { useUserPrivileges } from '../../common/components/user_privileges'; -import { RiskyHostLinks } from '../components/overview_risky_host_links'; import { useAlertsPrivileges } from '../../detections/containers/detection_engine/alerts/use_alerts_privileges'; -import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { LandingPageComponent } from '../../common/components/landing_page'; const OverviewComponent = () => { @@ -68,15 +66,6 @@ const OverviewComponent = () => { const { hasIndexRead, hasKibanaREAD } = useAlertsPrivileges(); const { tiDataSources: allTiDataSources, isInitiallyLoaded: isTiLoaded } = useAllTiDataSources(); - const riskyHostsEnabled = useIsExperimentalFeatureEnabled('riskyHostsEnabled'); - - const timerange = useMemo( - () => ({ - from, - to, - }), - [from, to] - ); return ( <> {indicesExist ? ( @@ -146,15 +135,6 @@ const OverviewComponent = () => { /> )} </EuiFlexItem> - <EuiFlexItem grow={1}> - {riskyHostsEnabled && ( - <RiskyHostLinks - deleteQuery={deleteQuery} - setQuery={setQuery} - timerange={timerange} - /> - )} - </EuiFlexItem> </EuiFlexGroup> </EuiFlexItem> </EuiFlexGroup> diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 490a68922b176b..7affdd066742f0 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -62,6 +62,7 @@ import { getLazyEndpointPolicyEditExtension } from './management/pages/policy/vi import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension'; import { getLazyEndpointPackageCustomExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension'; import { getLazyEndpointPolicyResponseExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_response_extension'; +import { getLazyEndpointGenericErrorsListExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_generic_errors_list'; import type { ExperimentalFeatures } from '../common/experimental_features'; import { parseExperimentalConfigValue } from '../common/experimental_features'; import { LazyEndpointCustomAssetsExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_custom_assets_extension'; @@ -232,6 +233,11 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S view: 'package-policy-response', Component: getLazyEndpointPolicyResponseExtension(core, plugins), }); + registerExtension({ + package: 'endpoint', + view: 'package-generic-errors-list', + Component: getLazyEndpointGenericErrorsListExtension(core, plugins), + }); } registerExtension({ diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap index b033febcd1ac8f..f6afb2bbe033ca 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap @@ -15,11 +15,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "A", - "process.name": "lsass.exe", + "process.name": "mimikatz.exe", "process.parent.entity_id": "", }, "id": "A", - "name": "lsass.exe", + "name": "mimikatz.exe", "parent": undefined, "stats": Object { "byCategory": Object {}, @@ -33,11 +33,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "A", - "process.name": "lsass.exe", + "process.name": "mimikatz.exe", "process.parent.entity_id": "", }, "id": "A", - "name": "lsass.exe", + "name": "mimikatz.exe", "parent": undefined, "stats": Object { "byCategory": Object {}, @@ -58,11 +58,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "A", - "process.name": "mimikatz.exe", + "process.name": "explorer.exe", "process.parent.entity_id": "", }, "id": "A", - "name": "mimikatz.exe", + "name": "explorer.exe", "parent": undefined, "stats": Object { "byCategory": Object {}, @@ -88,11 +88,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "C", - "process.name": "lsass.exe", + "process.name": "iexlorer.exe", "process.parent.entity_id": "A", }, "id": "C", - "name": "lsass.exe", + "name": "iexlorer.exe", "parent": "A", "stats": Object { "byCategory": Object {}, @@ -103,11 +103,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "I", - "process.name": "notepad.exe", + "process.name": "explorer.exe", "process.parent.entity_id": "A", }, "id": "I", - "name": "notepad.exe", + "name": "explorer.exe", "parent": "A", "stats": Object { "byCategory": Object {}, @@ -118,11 +118,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "D", - "process.name": "lsass.exe", + "process.name": "powershell.exe", "process.parent.entity_id": "B", }, "id": "D", - "name": "lsass.exe", + "name": "powershell.exe", "parent": "B", "stats": Object { "byCategory": Object {}, @@ -148,11 +148,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "F", - "process.name": "powershell.exe", + "process.name": "notepad.exe", "process.parent.entity_id": "C", }, "id": "F", - "name": "powershell.exe", + "name": "notepad.exe", "parent": "C", "stats": Object { "byCategory": Object {}, @@ -178,11 +178,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "H", - "process.name": "notepad.exe", + "process.name": "explorer.exe", "process.parent.entity_id": "G", }, "id": "H", - "name": "notepad.exe", + "name": "explorer.exe", "parent": "G", "stats": Object { "byCategory": Object {}, @@ -439,11 +439,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "A", - "process.name": "mimikatz.exe", + "process.name": "explorer.exe", "process.parent.entity_id": "", }, "id": "A", - "name": "mimikatz.exe", + "name": "explorer.exe", "parent": undefined, "stats": Object { "byCategory": Object {}, @@ -475,11 +475,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "C", - "process.name": "lsass.exe", + "process.name": "iexlorer.exe", "process.parent.entity_id": "A", }, "id": "C", - "name": "lsass.exe", + "name": "iexlorer.exe", "parent": "A", "stats": Object { "byCategory": Object {}, @@ -493,11 +493,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "I", - "process.name": "notepad.exe", + "process.name": "explorer.exe", "process.parent.entity_id": "A", }, "id": "I", - "name": "notepad.exe", + "name": "explorer.exe", "parent": "A", "stats": Object { "byCategory": Object {}, @@ -511,11 +511,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "D", - "process.name": "lsass.exe", + "process.name": "powershell.exe", "process.parent.entity_id": "B", }, "id": "D", - "name": "lsass.exe", + "name": "powershell.exe", "parent": "B", "stats": Object { "byCategory": Object {}, @@ -547,11 +547,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "F", - "process.name": "powershell.exe", + "process.name": "notepad.exe", "process.parent.entity_id": "C", }, "id": "F", - "name": "powershell.exe", + "name": "notepad.exe", "parent": "C", "stats": Object { "byCategory": Object {}, @@ -583,11 +583,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "H", - "process.name": "notepad.exe", + "process.name": "explorer.exe", "process.parent.entity_id": "G", }, "id": "H", - "name": "notepad.exe", + "name": "explorer.exe", "parent": "G", "stats": Object { "byCategory": Object {}, @@ -608,11 +608,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "A", - "process.name": "mimikatz.exe", + "process.name": "explorer.exe", "process.parent.entity_id": "", }, "id": "A", - "name": "mimikatz.exe", + "name": "explorer.exe", "parent": undefined, "stats": Object { "byCategory": Object {}, @@ -623,11 +623,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "B", - "process.name": "mimikatz.exe", + "process.name": "notepad.exe", "process.parent.entity_id": "A", }, "id": "B", - "name": "mimikatz.exe", + "name": "notepad.exe", "parent": "A", "stats": Object { "byCategory": Object {}, @@ -661,11 +661,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "A", - "process.name": "mimikatz.exe", + "process.name": "explorer.exe", "process.parent.entity_id": "", }, "id": "A", - "name": "mimikatz.exe", + "name": "explorer.exe", "parent": undefined, "stats": Object { "byCategory": Object {}, @@ -679,11 +679,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "B", - "process.name": "mimikatz.exe", + "process.name": "notepad.exe", "process.parent.entity_id": "A", }, "id": "B", - "name": "mimikatz.exe", + "name": "notepad.exe", "parent": "A", "stats": Object { "byCategory": Object {}, diff --git a/x-pack/plugins/security_solution/public/risk_score/containers/index.ts b/x-pack/plugins/security_solution/public/risk_score/containers/index.ts index 56e3ff14ce1484..323a6d26acb346 100644 --- a/x-pack/plugins/security_solution/public/risk_score/containers/index.ts +++ b/x-pack/plugins/security_solution/public/risk_score/containers/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { HostsRiskScore } from '../../../common/search_strategy/security_solution/risk_score'; +import type { HostRiskScore } from '../../../common/search_strategy/security_solution/risk_score'; export * from './all'; export * from './kpi'; @@ -25,5 +25,5 @@ export const enum HostRiskScoreQueryId { export interface HostRisk { loading: boolean; isModuleEnabled?: boolean; - result?: HostsRiskScore[]; + result?: HostRiskScore[]; } diff --git a/x-pack/plugins/security_solution/public/risk_score/containers/kpi/index.tsx b/x-pack/plugins/security_solution/public/risk_score/containers/kpi/index.tsx index 396d86e2d6acc7..6d4ee5c73c5f67 100644 --- a/x-pack/plugins/security_solution/public/risk_score/containers/kpi/index.tsx +++ b/x-pack/plugins/security_solution/public/risk_score/containers/kpi/index.tsx @@ -16,13 +16,13 @@ import { createFilter } from '../../../common/containers/helpers'; import type { KpiRiskScoreRequestOptions, KpiRiskScoreStrategyResponse, - RiskScoreAggByFields, } from '../../../../common/search_strategy'; import { getHostRiskIndex, getUserRiskIndex, RiskQueries, RiskSeverity, + RiskScoreEntity, } from '../../../../common/search_strategy'; import { useKibana } from '../../../common/lib/kibana'; @@ -32,7 +32,7 @@ import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_exper import type { SeverityCount } from '../../../common/components/severity/types'; import { useSpaceId } from '../../../common/hooks/use_space_id'; -type GetHostsRiskScoreProps = KpiRiskScoreRequestOptions & { +type GetHostRiskScoreProps = KpiRiskScoreRequestOptions & { data: DataPublicPluginStart; signal: AbortSignal; }; @@ -42,14 +42,14 @@ const getRiskScoreKpi = ({ defaultIndex, signal, filterQuery, - aggBy, -}: GetHostsRiskScoreProps): Observable<KpiRiskScoreStrategyResponse> => + entity, +}: GetHostRiskScoreProps): Observable<KpiRiskScoreStrategyResponse> => data.search.search<KpiRiskScoreRequestOptions, KpiRiskScoreStrategyResponse>( { defaultIndex, factoryQueryType: RiskQueries.kpiRiskScore, filterQuery: createFilter(filterQuery), - aggBy, + entity, }, { strategy: 'securitySolutionSearchStrategy', @@ -58,7 +58,7 @@ const getRiskScoreKpi = ({ ); const getRiskScoreKpiComplete = ( - props: GetHostsRiskScoreProps + props: GetHostRiskScoreProps ): Observable<KpiRiskScoreStrategyResponse> => { return getRiskScoreKpi(props).pipe( filter((response) => { @@ -80,11 +80,11 @@ interface RiskScoreKpi { type UseHostRiskScoreKpiProps = Omit< UseRiskScoreKpiProps, - 'defaultIndex' | 'aggBy' | 'featureEnabled' + 'defaultIndex' | 'aggBy' | 'featureEnabled' | 'entity' >; type UseUserRiskScoreKpiProps = Omit< UseRiskScoreKpiProps, - 'defaultIndex' | 'aggBy' | 'featureEnabled' + 'defaultIndex' | 'aggBy' | 'featureEnabled' | 'entity' >; export const useUserRiskScoreKpi = ({ @@ -99,7 +99,7 @@ export const useUserRiskScoreKpi = ({ filterQuery, skip, defaultIndex, - aggBy: 'user.name', + entity: RiskScoreEntity.user, featureEnabled: riskyUsersFeatureEnabled, }); }; @@ -116,7 +116,7 @@ export const useHostRiskScoreKpi = ({ filterQuery, skip, defaultIndex, - aggBy: 'host.name', + entity: RiskScoreEntity.host, featureEnabled: riskyHostsFeatureEnabled, }); }; @@ -125,7 +125,7 @@ interface UseRiskScoreKpiProps { filterQuery?: string | ESTermQuery; skip?: boolean; defaultIndex: string | undefined; - aggBy: RiskScoreAggByFields; + entity: RiskScoreEntity; featureEnabled: boolean; } @@ -133,7 +133,7 @@ const useRiskScoreKpi = ({ filterQuery, skip, defaultIndex, - aggBy, + entity, featureEnabled, }: UseRiskScoreKpiProps): RiskScoreKpi => { const { error, result, start, loading } = useRiskScoreKpiComplete(); @@ -146,10 +146,10 @@ const useRiskScoreKpi = ({ data, filterQuery, defaultIndex: [defaultIndex], - aggBy, + entity, }); } - }, [data, defaultIndex, start, filterQuery, skip, aggBy, featureEnabled]); + }, [data, defaultIndex, start, filterQuery, skip, entity, featureEnabled]); const severityCount = useMemo( () => ({ @@ -162,5 +162,6 @@ const useRiskScoreKpi = ({ }), [result] ); + return { error, severityCount, loading, isModuleDisabled }; }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx index 96ed68d32398db..0f7a5e0625216b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx @@ -185,7 +185,7 @@ describe('useFieldBrowserOptions', () => { }); it('should dispatch the proper action when a new field is saved', async () => { - let onSave: ((field: DataViewField) => void) | undefined; + let onSave: ((field: DataViewField[]) => void) | undefined; useKibanaMock().services.data.dataViews.get = () => Promise.resolve({} as DataView); useKibanaMock().services.dataViewFieldEditor.openEditor = (options) => { onSave = options.onSave; @@ -202,7 +202,7 @@ describe('useFieldBrowserOptions', () => { getByRole('button').click(); expect(onSave).toBeDefined(); - const savedField = { name: 'newField' } as DataViewField; + const savedField = [{ name: 'newField' }] as DataViewField[]; onSave!(savedField); await runAllPromises(); @@ -213,7 +213,7 @@ describe('useFieldBrowserOptions', () => { id: TimelineId.test, column: { columnHeaderType: defaultColumnHeaderType, - id: savedField.name, + id: savedField[0].name, initialWidth: DEFAULT_COLUMN_MIN_WIDTH, }, index: 0, @@ -222,7 +222,7 @@ describe('useFieldBrowserOptions', () => { }); it('should dispatch the proper actions when a field is edited', async () => { - let onSave: ((field: DataViewField) => void) | undefined; + let onSave: ((field: DataViewField[]) => void) | undefined; useKibanaMock().services.data.dataViews.get = () => Promise.resolve({} as DataView); useKibanaMock().services.dataViewFieldEditor.openEditor = (options) => { onSave = options.onSave; @@ -243,7 +243,7 @@ describe('useFieldBrowserOptions', () => { getByTestId('actionEditRuntimeField').click(); expect(onSave).toBeDefined(); - const savedField = { name: `new ${fieldItem.name}` } as DataViewField; + const savedField = [{ name: `new ${fieldItem.name}` }] as DataViewField[]; onSave!(savedField); await runAllPromises(); @@ -260,7 +260,7 @@ describe('useFieldBrowserOptions', () => { id: TimelineId.test, column: { columnHeaderType: defaultColumnHeaderType, - id: savedField.name, + id: savedField[0].name, initialWidth: DEFAULT_COLUMN_MIN_WIDTH, }, index: 0, diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx index 0d7a23800d4049..9fb4f2b13adb72 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx @@ -24,6 +24,8 @@ import { defaultColumnHeaderType } from '../timeline/body/column_headers/default import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; import { useCreateFieldButton } from './create_field_button'; import { useFieldTableColumns } from './field_table_columns'; +import { useStartTransaction } from '../../../common/lib/apm/use_start_transaction'; +import { FIELD_BROWSER_ACTIONS } from '../../../common/lib/apm/user_actions'; export type FieldEditorActions = { closeEditor: () => void } | null; export type FieldEditorActionsRef = MutableRefObject<FieldEditorActions>; @@ -50,6 +52,7 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ const dispatch = useDispatch(); const [dataView, setDataView] = useState<DataView | null>(null); + const { startTransaction } = useStartTransaction(); const { indexFieldsSearch } = useDataView(); const { dataViewFieldEditor, @@ -74,33 +77,36 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ const closeFieldEditor = dataViewFieldEditor.openEditor({ ctx: { dataView }, fieldName, - onSave: async (savedField: DataViewField) => { + onSave: async (savedFields: DataViewField[]) => { + startTransaction({ name: FIELD_BROWSER_ACTIONS.FIELD_SAVED }); // Fetch the updated list of fields // Using cleanCache since the number of fields might have not changed, but we need to update the state anyway await indexFieldsSearch({ dataViewId: selectedDataViewId, cleanCache: true }); - if (fieldName && fieldName !== savedField.name) { - // Remove old field from event table when renaming a field + for (const savedField of savedFields) { + if (fieldName && fieldName !== savedField.name) { + // Remove old field from event table when renaming a field + dispatch( + removeColumn({ + columnId: fieldName, + id: timelineId, + }) + ); + } + + // Add the saved column field to the table in any case dispatch( - removeColumn({ - columnId: fieldName, + upsertColumn({ + column: { + columnHeaderType: defaultColumnHeaderType, + id: savedField.name, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, id: timelineId, + index: 0, }) ); } - - // Add the saved column field to the table in any case - dispatch( - upsertColumn({ - column: { - columnHeaderType: defaultColumnHeaderType, - id: savedField.name, - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - id: timelineId, - index: 0, - }) - ); if (editorActionsRef) { editorActionsRef.current = null; } @@ -124,6 +130,7 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ indexFieldsSearch, dispatch, timelineId, + startTransaction, ] ); @@ -134,6 +141,8 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ ctx: { dataView }, fieldName, onDelete: async () => { + startTransaction({ name: FIELD_BROWSER_ACTIONS.FIELD_DELETED }); + // Fetch the updated list of fields await indexFieldsSearch({ dataViewId: selectedDataViewId }); @@ -147,7 +156,15 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ }); } }, - [dataView, selectedDataViewId, dataViewFieldEditor, indexFieldsSearch, dispatch, timelineId] + [ + dataView, + selectedDataViewId, + dataViewFieldEditor, + indexFieldsSearch, + dispatch, + timelineId, + startTransaction, + ] ); const hasFieldEditPermission = useMemo( diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index 1dd795bd795b59..628d2bf14d5e53 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -48,6 +48,8 @@ import { deleteTimelinesByIds } from '../../containers/api'; import type { Direction } from '../../../../common/search_strategy'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { useSourcererDataView } from '../../../common/containers/sourcerer'; +import { useStartTransaction } from '../../../common/lib/apm/use_start_transaction'; +import { TIMELINE_ACTIONS } from '../../../common/lib/apm/user_actions'; interface OwnProps<TCache = object> { /** Displays open timeline in modal */ @@ -86,6 +88,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>( title, }) => { const dispatch = useDispatch(); + const { startTransaction } = useStartTransaction(); /** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */ const [itemIdToExpandedNotesRowMap, setItemIdToExpandedNotesRowMap] = useState< Record<string, JSX.Element> @@ -197,6 +200,10 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>( const deleteTimelines: DeleteTimelines = useCallback( async (timelineIds: string[]) => { + startTransaction({ + name: timelineIds.length > 1 ? TIMELINE_ACTIONS.BULK_DELETE : TIMELINE_ACTIONS.DELETE, + }); + if (timelineIds.includes(timelineSavedObjectId)) { dispatch( dispatchCreateNewTimeline({ @@ -212,7 +219,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>( await deleteTimelinesByIds(timelineIds); refetch(); }, - [timelineSavedObjectId, refetch, dispatch, dataViewId, selectedPatterns] + [startTransaction, timelineSavedObjectId, refetch, dispatch, dataViewId, selectedPatterns] ); const onDeleteOneTimeline: OnDeleteOneTimeline = useCallback( @@ -274,6 +281,10 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>( const openTimeline: OnOpenTimeline = useCallback( ({ duplicate, timelineId, timelineType: timelineTypeToOpen }) => { + if (duplicate) { + startTransaction({ name: TIMELINE_ACTIONS.DUPLICATE }); + } + if (isModal && closeModalTimeline != null) { closeModalTimeline(); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx index b34338b4cbce96..065ac297ee468b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx @@ -11,8 +11,9 @@ import type { TimelineEventsDetailsItem } from '../../../../../common/search_str import { getFieldValue } from '../../../../detections/components/host_isolation/helpers'; import { DEFAULT_ALERTS_INDEX, DEFAULT_PREVIEW_INDEX } from '../../../../../common/constants'; -interface GetBasicDataFromDetailsData { +export interface GetBasicDataFromDetailsData { alertId: string; + agentId?: string; isAlert: boolean; hostName: string; ruleName: string; @@ -31,6 +32,11 @@ export const useBasicDataFromDetailsData = ( const alertId = useMemo(() => getFieldValue({ category: '_id', field: '_id' }, data), [data]); + const agentId = useMemo( + () => getFieldValue({ category: 'agent', field: 'agent.id' }, data), + [data] + ); + const hostName = useMemo( () => getFieldValue({ category: 'host', field: 'host.name' }, data), [data] @@ -44,17 +50,18 @@ export const useBasicDataFromDetailsData = ( return useMemo( () => ({ alertId, + agentId, isAlert, hostName, ruleName, timestamp, }), - [alertId, hostName, isAlert, ruleName, timestamp] + [agentId, alertId, hostName, isAlert, ruleName, timestamp] ); }; /* -The referenced alert _index in the flyout uses the `.internal.` such as +The referenced alert _index in the flyout uses the `.internal.` such as `.internal.alerts-security.alerts-spaceId` in the alert page flyout and .internal.preview.alerts-security.alerts-spaceId` in the rule creation preview flyout but we always want to use their respective aliase indices rather than accessing their backing .internal. indices. diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx index 83e06db651e00e..00397ea43e59ab 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiSpacer } from '@elastic/eui'; +import { EuiSpacer, EuiFlyoutBody } from '@elastic/eui'; import React, { useMemo } from 'react'; import deepEqual from 'fast-deep-equal'; @@ -22,6 +22,8 @@ import { useHostIsolationTools } from './use_host_isolation_tools'; import { FlyoutBody, FlyoutHeader, FlyoutFooter } from './flyout'; import { useBasicDataFromDetailsData, getAlertIndexAlias } from './helpers'; import { useSpaceId } from '../../../../common/hooks/use_space_id'; +import { EndpointIsolateSuccess } from '../../../../common/components/endpoint/host_isolation'; +import { HostIsolationPanel } from '../../../../detections/components/host_isolation'; interface EventDetailsPanelProps { browserFields: BrowserFields; @@ -95,49 +97,144 @@ const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({ pagination, }); - const hostRisk: HostRisk | null = data - ? { - loading: hostRiskLoading, - isModuleEnabled, - result: data, - } - : null; + const hostRisk: HostRisk | null = useMemo(() => { + return data + ? { + loading: hostRiskLoading, + isModuleEnabled, + result: data, + } + : null; + }, [data, hostRiskLoading, isModuleEnabled]); + + const header = useMemo( + () => + isFlyoutView || isHostIsolationPanelOpen ? ( + <FlyoutHeader + isHostIsolationPanelOpen={isHostIsolationPanelOpen} + isAlert={isAlert} + isolateAction={isolateAction} + loading={loading} + ruleName={ruleName} + showAlertDetails={showAlertDetails} + timestamp={timestamp} + /> + ) : ( + <ExpandableEventTitle + isAlert={isAlert} + loading={loading} + ruleName={ruleName} + handleOnEventClosed={handleOnEventClosed} + /> + ), + [ + handleOnEventClosed, + isAlert, + isFlyoutView, + isHostIsolationPanelOpen, + isolateAction, + loading, + ruleName, + showAlertDetails, + timestamp, + ] + ); + + const body = useMemo(() => { + if (isFlyoutView) { + return ( + <FlyoutBody + alertId={alertId} + browserFields={browserFields} + detailsData={detailsData} + event={expandedEvent} + hostName={hostName} + hostRisk={hostRisk} + handleIsolationActionSuccess={handleIsolationActionSuccess} + handleOnEventClosed={handleOnEventClosed} + isAlert={isAlert} + isDraggable={isDraggable} + isolateAction={isolateAction} + isIsolateActionSuccessBannerVisible={isIsolateActionSuccessBannerVisible} + isHostIsolationPanelOpen={isHostIsolationPanelOpen} + loading={loading} + rawEventData={rawEventData} + showAlertDetails={showAlertDetails} + timelineId={timelineId} + isReadOnly={isReadOnly} + /> + ); + } else if (isHostIsolationPanelOpen) { + return ( + <> + {isIsolateActionSuccessBannerVisible && ( + <EndpointIsolateSuccess + hostName={hostName} + alertId={alertId} + isolateAction={isolateAction} + /> + )} + <EuiFlyoutBody> + <HostIsolationPanel + details={detailsData} + cancelCallback={showAlertDetails} + successCallback={handleIsolationActionSuccess} + isolateAction={isolateAction} + /> + </EuiFlyoutBody> + </> + ); + } else { + return ( + <> + <EuiSpacer size="m" /> + <ExpandableEvent + browserFields={browserFields} + detailsData={detailsData} + event={expandedEvent} + isAlert={isAlert} + isDraggable={isDraggable} + loading={loading} + rawEventData={rawEventData} + timelineId={timelineId} + timelineTabType={tabType} + hostRisk={hostRisk} + handleOnEventClosed={handleOnEventClosed} + /> + </> + ); + } + }, [ + alertId, + browserFields, + detailsData, + expandedEvent, + handleIsolationActionSuccess, + handleOnEventClosed, + hostName, + hostRisk, + isAlert, + isDraggable, + isFlyoutView, + isHostIsolationPanelOpen, + isIsolateActionSuccessBannerVisible, + isReadOnly, + isolateAction, + loading, + rawEventData, + showAlertDetails, + tabType, + timelineId, + ]); if (!expandedEvent?.eventId) { return null; } - return isFlyoutView ? ( + return ( <> - <FlyoutHeader - isHostIsolationPanelOpen={isHostIsolationPanelOpen} - isAlert={isAlert} - isolateAction={isolateAction} - loading={loading} - ruleName={ruleName} - showAlertDetails={showAlertDetails} - timestamp={timestamp} - /> - <FlyoutBody - alertId={alertId} - browserFields={browserFields} - detailsData={detailsData} - event={expandedEvent} - hostName={hostName} - hostRisk={hostRisk} - handleIsolationActionSuccess={handleIsolationActionSuccess} - handleOnEventClosed={handleOnEventClosed} - isAlert={isAlert} - isDraggable={isDraggable} - isolateAction={isolateAction} - isIsolateActionSuccessBannerVisible={isIsolateActionSuccessBannerVisible} - isHostIsolationPanelOpen={isHostIsolationPanelOpen} - loading={loading} - rawEventData={rawEventData} - showAlertDetails={showAlertDetails} - timelineId={timelineId} - isReadOnly={isReadOnly} - /> + {header} + {body} <FlyoutFooter detailsData={detailsData} detailsEcsData={ecsData} @@ -151,41 +248,6 @@ const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({ timelineId={timelineId} /> </> - ) : ( - <> - <ExpandableEventTitle - isAlert={isAlert} - loading={loading} - ruleName={ruleName} - handleOnEventClosed={handleOnEventClosed} - /> - <EuiSpacer size="m" /> - <ExpandableEvent - browserFields={browserFields} - detailsData={detailsData} - event={expandedEvent} - isAlert={isAlert} - isDraggable={isDraggable} - loading={loading} - rawEventData={rawEventData} - timelineId={timelineId} - timelineTabType={tabType} - hostRisk={hostRisk} - handleOnEventClosed={handleOnEventClosed} - /> - <FlyoutFooter - detailsData={detailsData} - detailsEcsData={ecsData} - expandedEvent={expandedEvent} - handleOnEventClosed={handleOnEventClosed} - isHostIsolationPanelOpen={isHostIsolationPanelOpen} - isReadOnly={isReadOnly} - loadingEventDetails={loading} - onAddIsolationStatusClick={showHostIsolationPanel} - refetchFlyoutData={refetchFlyoutData} - timelineId={timelineId} - /> - </> ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index 1e53ba23c39af3..ea7d6e0ef46876 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -39,6 +39,8 @@ import { TimelineId, TimelineTabs } from '../../../../../../common/types/timelin import { timelineActions, timelineSelectors } from '../../../../store/timeline'; import { timelineDefaults } from '../../../../store/timeline/defaults'; import { isInvestigateInResolverActionEnabled } from '../../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver'; +import { useStartTransaction } from '../../../../../common/lib/apm/use_start_transaction'; +import { ALERTS_ACTIONS } from '../../../../../common/lib/apm/user_actions'; export const isAlert = (eventType: TimelineEventsType | Omit<TimelineEventsType, 'all'>): boolean => eventType === 'signal'; @@ -71,6 +73,7 @@ const ActionsComponent: React.FC<ActionProps> = ({ const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); const emptyNotes: string[] = []; const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const { startTransaction } = useStartTransaction(); const onPinEvent: OnPinEvent = useCallback( (evtId) => dispatch(timelineActions.pinEvent({ id: timelineId, eventId: evtId })), @@ -118,6 +121,8 @@ const ActionsComponent: React.FC<ActionProps> = ({ const { setGlobalFullScreen } = useGlobalFullScreen(); const { setTimelineFullScreen } = useTimelineFullScreen(); const handleClick = useCallback(() => { + startTransaction({ name: ALERTS_ACTIONS.OPEN_ANALYZER }); + const dataGridIsFullScreen = document.querySelector('.euiDataGrid--fullScreen'); dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: ecsData._id })); if (timelineId === TimelineId.active) { @@ -130,7 +135,14 @@ const ActionsComponent: React.FC<ActionProps> = ({ setGlobalFullScreen(true); } } - }, [dispatch, ecsData._id, timelineId, setGlobalFullScreen, setTimelineFullScreen]); + }, [ + startTransaction, + dispatch, + timelineId, + ecsData._id, + setTimelineFullScreen, + setGlobalFullScreen, + ]); const sessionViewConfig = useMemo(() => { const { process, _id, timestamp } = ecsData; @@ -155,6 +167,8 @@ const ActionsComponent: React.FC<ActionProps> = ({ const openSessionView = useCallback(() => { const dataGridIsFullScreen = document.querySelector('.euiDataGrid--fullScreen'); + startTransaction({ name: ALERTS_ACTIONS.OPEN_SESSION_VIEW }); + if (timelineId === TimelineId.active) { if (dataGridIsFullScreen) { setTimelineFullScreen(true); @@ -170,7 +184,14 @@ const ActionsComponent: React.FC<ActionProps> = ({ if (sessionViewConfig !== null) { dispatch(updateTimelineSessionViewConfig({ id: timelineId, sessionViewConfig })); } - }, [dispatch, timelineId, sessionViewConfig, setGlobalFullScreen, setTimelineFullScreen]); + }, [ + startTransaction, + timelineId, + sessionViewConfig, + setTimelineFullScreen, + dispatch, + setGlobalFullScreen, + ]); return ( <ActionsContainer> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx index bd267abb23c7db..d887b72cdb33a8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx @@ -30,6 +30,8 @@ import { useCreateTimeline } from '../properties/use_create_timeline'; import * as commonI18n from '../properties/translations'; import * as i18n from './translations'; import { formSchema } from './schema'; +import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; +import { TIMELINE_ACTIONS } from '../../../../common/lib/apm/user_actions'; const CommonUseField = getUseField({ component: Field }); interface TimelineTitleAndDescriptionProps { @@ -44,6 +46,7 @@ interface TimelineTitleAndDescriptionProps { // the unsaved timeline / template export const TimelineTitleAndDescription = React.memo<TimelineTitleAndDescriptionProps>( ({ closeSaveTimeline, initialFocus, timelineId, showWarning }) => { + const { startTransaction } = useStartTransaction(); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const { isSaving, @@ -99,6 +102,11 @@ export const TimelineTitleAndDescription = React.memo<TimelineTitleAndDescriptio }); const { isSubmitted, isSubmitting, submit } = form; + const onSubmit = useCallback(() => { + startTransaction({ name: TIMELINE_ACTIONS.SAVE }); + submit(); + }, [submit, startTransaction]); + const handleCancel = useCallback(() => { if (showWarning) { handleCreateNewTimeline(); @@ -236,7 +244,7 @@ export const TimelineTitleAndDescription = React.memo<TimelineTitleAndDescriptio size="s" isDisabled={isSaving || isSubmitting} fill={true} - onClick={submit} + onClick={onSubmit} data-test-subj="save-button" > {saveButtonTitle} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx index a1fcefceadfb5b..3cf7acdef93fb3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx @@ -18,8 +18,6 @@ import { FilterStateStore } from '@kbn/es-query'; import { FilterManager } from '@kbn/data-plugin/public'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; import { buildGlobalQuery } from '../helpers'; -import { setAutocomplete } from '@kbn/unified-search-plugin/public/services'; -import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import type { QueryBarTimelineComponentProps } from '.'; import { QueryBarTimeline, getDataProviderFilter, TIMELINE_FILTER_DROP_AREA } from '.'; @@ -182,11 +180,6 @@ describe('Timeline QueryBar ', () => { }); describe('#onSavedQuery', () => { - beforeEach(() => { - const autocompleteStart = unifiedSearchPluginMock.createStartContract(); - setAutocomplete(autocompleteStart.autocomplete); - }); - test('is only reference that changed when dataProviders props get updated', async () => { const Proxy = (props: QueryBarTimelineComponentProps) => ( <TestProviders> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx index 9b633b0baed7f5..407a66086805fc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx @@ -362,11 +362,11 @@ const TabsContentComponent: React.FC<BasicTimelineTab> = ({ data-test-subj={`timelineTabs-${TimelineTabs.notes}`} onClick={setNotesAsActiveTab} isSelected={activeTab === TimelineTabs.notes} - disabled={false} + disabled={timelineType === TimelineType.template} key={TimelineTabs.notes} > <span>{i18n.NOTES_TAB}</span> - {showTimeline && numberOfNotes > 0 && ( + {showTimeline && numberOfNotes > 0 && timelineType === TimelineType.default && ( <div> <CountBadge>{numberOfNotes}</CountBadge> </div> @@ -375,11 +375,12 @@ const TabsContentComponent: React.FC<BasicTimelineTab> = ({ <StyledEuiTab data-test-subj={`timelineTabs-${TimelineTabs.pinned}`} onClick={setPinnedAsActiveTab} + disabled={timelineType === TimelineType.template} isSelected={activeTab === TimelineTabs.pinned} key={TimelineTabs.pinned} > <span>{i18n.PINNED_TAB}</span> - {showTimeline && numberOfPinnedEvents > 0 && ( + {showTimeline && numberOfPinnedEvents > 0 && timelineType === TimelineType.default && ( <div> <CountBadge>{numberOfPinnedEvents}</CountBadge> </div> diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx index 00197a8719a6d8..9ef5fbfd0a203c 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx @@ -28,6 +28,7 @@ const mockEvents = mockTimelineData.filter((i, index) => index <= 11); const mockSearch = jest.fn(); +jest.mock('../../common/lib/apm/use_track_http_request'); jest.mock('../../common/hooks/use_experimental_features'); const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 6024c4613f265c..700d5d9d1255e3 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -43,6 +43,8 @@ import type { TimelineEqlResponse, } from '../../../common/search_strategy/timeline/events/eql'; import { useAppToasts } from '../../common/hooks/use_app_toasts'; +import { useTrackHttpRequest } from '../../common/lib/apm/use_track_http_request'; +import { APP_UI_ID } from '../../../common/constants'; export interface TimelineArgs { events: TimelineItem[]; @@ -156,6 +158,7 @@ export const useTimelineEvents = ({ null ); const prevTimelineRequest = useRef<TimelineRequest<typeof language> | null>(null); + const { startTracking } = useTrackHttpRequest(); const clearSignalsState = useCallback(() => { if (id != null && detectionsTimelineIds.some((timelineId) => timelineId === id)) { @@ -219,6 +222,8 @@ export const useTimelineEvents = ({ prevTimelineRequest.current = request; abortCtrl.current = new AbortController(); setLoading(true); + const { endTracking } = startTracking({ name: `${APP_UI_ID} timeline events search` }); + searchSubscription$.current = data.search .search<TimelineRequest<typeof language>, TimelineResponse<typeof language>>(request, { strategy: @@ -230,6 +235,7 @@ export const useTimelineEvents = ({ .subscribe({ next: (response) => { if (isCompleteResponse(response)) { + endTracking('success'); setLoading(false); setTimelineResponse((prevResponse) => { const newTimelineResponse = { @@ -257,12 +263,14 @@ export const useTimelineEvents = ({ }); searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { + endTracking('invalid'); setLoading(false); addWarning(i18n.ERROR_TIMELINE_EVENTS); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { + endTracking(abortCtrl.current.signal.aborted ? 'aborted' : 'error'); setLoading(false); data.search.showError(msg); searchSubscription$.current.unsubscribe(); @@ -317,6 +325,7 @@ export const useTimelineEvents = ({ pageName, skip, id, + startTracking, data.search, dataViewId, setUpdated, diff --git a/x-pack/plugins/security_solution/public/users/components/user_risk_information/index.tsx b/x-pack/plugins/security_solution/public/users/components/user_risk_information/index.tsx index 07fd273625d932..5da702eb877979 100644 --- a/x-pack/plugins/security_solution/public/users/components/user_risk_information/index.tsx +++ b/x-pack/plugins/security_solution/public/users/components/user_risk_information/index.tsx @@ -105,9 +105,9 @@ const UserRiskInformationFlyout = ({ handleOnClose }: { handleOnClose: () => voi <EuiSpacer size="l" /> <FormattedMessage id="xpack.securitySolution.users.userRiskInformation.learnMore" - defaultMessage="You can learn more about user risk {usersRiskScoreDocumentationLink}" + defaultMessage="You can learn more about user risk {UserRiskScoreDocumentationLink}" values={{ - usersRiskScoreDocumentationLink: ( + UserRiskScoreDocumentationLink: ( <EuiLink href={RISKY_USERS_DOC_LINK} target="_blank"> <FormattedMessage id="xpack.securitySolution.users.userRiskInformation.link" diff --git a/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/columns.test.tsx b/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/columns.test.tsx index 6cde49e39c3907..8192c2a8e7f5cf 100644 --- a/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/columns.test.tsx +++ b/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/columns.test.tsx @@ -9,6 +9,7 @@ import { render } from '@testing-library/react'; import type { UserRiskScoreColumns } from '.'; import { getUserRiskScoreColumns } from './columns'; import { TestProviders } from '../../../common/mock'; +import { RiskScoreFields } from '../../../../common/search_strategy'; describe('getUserRiskScoreColumns', () => { const defaultProps = { @@ -19,8 +20,8 @@ describe('getUserRiskScoreColumns', () => { const columns = getUserRiskScoreColumns(defaultProps); expect(columns[0].field).toBe('user.name'); - expect(columns[1].field).toBe('risk_stats.risk_score'); - expect(columns[2].field).toBe('risk'); + expect(columns[1].field).toBe(RiskScoreFields.userRiskScore); + expect(columns[2].field).toBe(RiskScoreFields.userRisk); columns.forEach((column) => { expect(column).toHaveProperty('name'); diff --git a/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/columns.tsx b/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/columns.tsx index 24ccfd3eb01f5d..c50ae488383f07 100644 --- a/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/columns.tsx @@ -21,6 +21,7 @@ import type { UserRiskScoreColumns } from '.'; import * as i18n from './translations'; import { RiskScore } from '../../../common/components/severity/common'; import type { RiskSeverity } from '../../../../common/search_strategy'; +import { RiskScoreFields } from '../../../../common/search_strategy'; import { UserDetailsLink } from '../../../common/components/links'; import { UsersTableType } from '../../store/model'; @@ -68,7 +69,7 @@ export const getUserRiskScoreColumns = ({ }, }, { - field: 'risk_stats.risk_score', + field: RiskScoreFields.userRiskScore, name: i18n.USER_RISK_SCORE, truncateText: true, mobileOptions: { show: true }, @@ -85,7 +86,7 @@ export const getUserRiskScoreColumns = ({ }, }, { - field: 'risk', + field: RiskScoreFields.userRisk, name: ( <EuiToolTip content={i18n.USER_RISK_TOOLTIP}> <> diff --git a/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.test.tsx b/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.test.tsx index c0cd2e351298e4..34f0116bb055e1 100644 --- a/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.test.tsx @@ -9,6 +9,8 @@ import { render } from '@testing-library/react'; import { noop } from 'lodash'; import React from 'react'; import { UserRiskScoreTable } from '.'; +import type { UserRiskScore } from '../../../../common/search_strategy'; +import { RiskSeverity } from '../../../../common/search_strategy'; import { TestProviders } from '../../../common/mock'; import { UsersType } from '../../store/model'; @@ -18,16 +20,17 @@ describe('UserRiskScoreTable', () => { data: [ { '@timestamp': '1641902481', - risk: 'High', - risk_stats: { - rule_risks: [], - risk_score: 71, - }, user: { name: username, + risk: { + rule_risks: [], + calculated_score_norm: 71, + calculated_level: RiskSeverity.high, + multipliers: [], + }, }, }, - ], + ] as UserRiskScore[], id: 'test_id', isInspect: false, loading: false, diff --git a/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.tsx b/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.tsx index 96a81ab4f50733..245150f4fb49db 100644 --- a/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.tsx +++ b/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.tsx @@ -18,10 +18,7 @@ import { getUserRiskScoreColumns } from './columns'; import * as i18nUsers from '../../pages/translations'; import * as i18n from './translations'; import { usersModel, usersSelectors, usersActions } from '../../store'; -import type { - UserRiskScoreFields, - UserRiskScoreItem, -} from '../../../../common/search_strategy/security_solution/users/common'; +import type { UserRiskScoreItem } from '../../../../common/search_strategy/security_solution/users/common'; import type { SeverityCount } from '../../../common/components/severity/types'; import { SeverityBadges } from '../../../common/components/severity/severity_badges'; import { SeverityBar } from '../../../common/components/severity/severity_bar'; @@ -29,9 +26,10 @@ import { SeverityFilterGroup } from '../../../common/components/severity/severit import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import type { State } from '../../../common/store'; import type { + RiskScoreFields, RiskScoreSortField, RiskSeverity, - UsersRiskScore, + UserRiskScore, } from '../../../../common/search_strategy'; const IconWrapper = styled.span` @@ -52,7 +50,7 @@ export const rowItems: ItemsPerRow[] = [ const tableType = usersModel.UsersTableType.risk; interface UserRiskScoreTableProps { - data: UsersRiskScore[]; + data: UserRiskScore[]; id: string; isInspect: boolean; loading: boolean; @@ -64,9 +62,9 @@ interface UserRiskScoreTableProps { } export type UserRiskScoreColumns = [ - Columns<UserRiskScoreItem[UserRiskScoreFields.userName]>, - Columns<UserRiskScoreItem[UserRiskScoreFields.riskScore]>, - Columns<UserRiskScoreItem[UserRiskScoreFields.risk]> + Columns<UserRiskScoreItem[RiskScoreFields.userName]>, + Columns<UserRiskScoreItem[RiskScoreFields.userRiskScore]>, + Columns<UserRiskScoreItem[RiskScoreFields.userRisk]> ]; const UserRiskScoreTableComponent: React.FC<UserRiskScoreTableProps> = ({ @@ -170,7 +168,7 @@ const UserRiskScoreTableComponent: React.FC<UserRiskScoreTableProps> = ({ ); const getUserRiskScoreFilterQuerySelector = useMemo( - () => usersSelectors.usersRiskScoreSeverityFilterSelector(), + () => usersSelectors.userRiskScoreSeverityFilterSelector(), [] ); const severitySelectionRedux = useDeepEqualSelector((state: State) => diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.tsx b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.tsx index cef4740500a979..de45a4cdeeba4c 100644 --- a/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.tsx @@ -16,6 +16,7 @@ import { RiskScoreOverTime } from '../../../common/components/risk_score_over_ti import { TopRiskScoreContributors } from '../../../common/components/top_risk_score_contributors'; import { useQueryToggle } from '../../../common/containers/query_toggle'; import { UserRiskScoreQueryId, useUserRiskScore } from '../../../risk_score/containers'; +import type { UserRiskScore } from '../../../../common/search_strategy'; import { buildUserNamesFilter } from '../../../../common/search_strategy'; import type { UsersComponentsQueryProps } from './types'; import { UserRiskInformationButtonEmpty } from '../../components/user_risk_information'; @@ -86,7 +87,9 @@ const UserRiskTabBodyComponent: React.FC< [setOverTimeToggleStatus] ); - const rules = data && data.length > 0 ? data[data.length - 1].risk_stats.rule_risks : []; + const lastUsertRiskItem: UserRiskScore | null = + data && data.length > 0 ? data[data.length - 1] : null; + const rules = lastUsertRiskItem ? lastUsertRiskItem.user.risk.rule_risks : []; return ( <> diff --git a/x-pack/plugins/security_solution/public/users/pages/users.tsx b/x-pack/plugins/security_solution/public/users/pages/users.tsx index 01b239ee89b489..94345fa24f3777 100644 --- a/x-pack/plugins/security_solution/public/users/pages/users.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/users.tsx @@ -45,7 +45,7 @@ import { useDeepEqualSelector } from '../../common/hooks/use_selector'; import { useInvalidFilterQuery } from '../../common/hooks/use_invalid_filter_query'; import { UsersKpiComponent } from '../components/kpi_users'; import type { UpdateDateRange } from '../../common/components/charts/common'; -import { LastEventIndexKey } from '../../../common/search_strategy'; +import { LastEventIndexKey, RiskScoreEntity } from '../../../common/search_strategy'; import { generateSeverityFilter } from '../../hosts/store/helpers'; import { UsersTableType } from '../store/model'; import { hasMlUserPermissions } from '../../../common/machine_learning/has_ml_user_permissions'; @@ -77,12 +77,12 @@ const UsersComponent = () => { const query = useDeepEqualSelector(getGlobalQuerySelector); const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); - const getUsersRiskScoreFilterQuerySelector = useMemo( - () => usersSelectors.usersRiskScoreSeverityFilterSelector(), + const getUserRiskScoreFilterQuerySelector = useMemo( + () => usersSelectors.userRiskScoreSeverityFilterSelector(), [] ); const severitySelection = useDeepEqualSelector((state: State) => - getUsersRiskScoreFilterQuerySelector(state) + getUserRiskScoreFilterQuerySelector(state) ); const { to, from, deleteQuery, setQuery, isInitializing } = useGlobalTime(); @@ -96,7 +96,7 @@ const UsersComponent = () => { } if (tabName === UsersTableType.risk) { - const severityFilter = generateSeverityFilter(severitySelection); + const severityFilter = generateSeverityFilter(severitySelection, RiskScoreEntity.user); return [...severityFilter, ...filters]; } diff --git a/x-pack/plugins/security_solution/public/users/store/model.ts b/x-pack/plugins/security_solution/public/users/store/model.ts index de9606d1639446..bee5eca0d71987 100644 --- a/x-pack/plugins/security_solution/public/users/store/model.ts +++ b/x-pack/plugins/security_solution/public/users/store/model.ts @@ -37,7 +37,7 @@ export interface AllUsersQuery extends BasicQueryPaginated { sort: SortUsersField; } -export interface UsersRiskScoreQuery extends BasicQueryPaginated { +export interface UserRiskScoreQuery extends BasicQueryPaginated { sort: RiskScoreSortField; severitySelection: RiskSeverity[]; } @@ -51,7 +51,7 @@ export interface UsersQueries { [UsersTableType.allUsers]: AllUsersQuery; [UsersTableType.authentications]: BasicQueryPaginated; [UsersTableType.anomalies]: UsersAnomaliesQuery; - [UsersTableType.risk]: UsersRiskScoreQuery; + [UsersTableType.risk]: UserRiskScoreQuery; [UsersTableType.events]: BasicQueryPaginated; } diff --git a/x-pack/plugins/security_solution/public/users/store/reducer.ts b/x-pack/plugins/security_solution/public/users/store/reducer.ts index 0699f3d3c3acc3..79e9511bbd6f0c 100644 --- a/x-pack/plugins/security_solution/public/users/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/users/store/reducer.ts @@ -44,7 +44,7 @@ export const initialUsersState: UsersModel = { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, sort: { - field: RiskScoreFields.riskScore, + field: RiskScoreFields.userRiskScore, direction: Direction.desc, }, severitySelection: [], diff --git a/x-pack/plugins/security_solution/public/users/store/selectors.ts b/x-pack/plugins/security_solution/public/users/store/selectors.ts index db054c88cf3add..eb69c941fa2361 100644 --- a/x-pack/plugins/security_solution/public/users/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/users/store/selectors.ts @@ -23,7 +23,7 @@ export const allUsersSelector = () => export const userRiskScoreSelector = () => createSelector(selectUserPage, (users) => users.queries[UsersTableType.risk]); -export const usersRiskScoreSeverityFilterSelector = () => +export const userRiskScoreSeverityFilterSelector = () => createSelector(selectUserPage, (users) => users.queries[UsersTableType.risk].severitySelection); export const authenticationsSelector = () => diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts index 72dbf3ade5e4a2..213f839421a71f 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts @@ -8,6 +8,7 @@ import { Client } from '@elastic/elasticsearch'; import { ToolingLog } from '@kbn/tooling-log'; import { KbnClient } from '@kbn/test'; +import type { StatusResponse } from '@kbn/core-status-common-internal'; import { createSecuritySuperuser } from './security_user_services'; export interface RuntimeServices { @@ -116,3 +117,24 @@ export const createKbnClient = ({ return new KbnClient({ log, url: kbnUrl }); }; + +/** + * Retrieves the Stack (kibana/ES) version from the `/api/status` kibana api + * @param kbnClient + */ +export const fetchStackVersion = async (kbnClient: KbnClient): Promise<string> => { + const status = ( + await kbnClient.request<StatusResponse>({ + method: 'GET', + path: '/api/status', + }) + ).data; + + if (!status?.version?.number) { + throw new Error( + `unable to get stack version from '/api/status' \n${JSON.stringify(status, null, 2)}` + ); + } + + return status.version.number; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts index 9eb03dd80e3269..a871151ed0b0d3 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts @@ -5,7 +5,7 @@ * 2.0. */ -/* eslint-disable no-console */ +/* eslint-disable no-console,max-classes-per-file */ import yargs from 'yargs'; import fs from 'fs'; import { Client, errors } from '@elastic/elasticsearch'; @@ -14,8 +14,10 @@ import { CA_CERT_PATH } from '@kbn/dev-utils'; import { ToolingLog } from '@kbn/tooling-log'; import type { KbnClientOptions } from '@kbn/test'; import { KbnClient } from '@kbn/test'; +import { EndpointMetadataGenerator } from '../../common/endpoint/data_generators/endpoint_metadata_generator'; import { indexHostsAndAlerts } from '../../common/endpoint/index_data'; import { ANCESTRY_LIMIT, EndpointDocGenerator } from '../../common/endpoint/generate_data'; +import { fetchStackVersion } from './common/stack_services'; main(); @@ -249,6 +251,13 @@ async function main() { type: 'string', default: '', }, + randomVersions: { + describe: + 'By default, the data generated (that contains a stack version - ex: `agent.version`) will have a ' + + 'version number set to be the same as the version of the running stack. Using this flag (`--randomVersions=true`) ' + + 'will result in random version being generated', + default: false, + }, }).argv; let ca: Buffer; @@ -323,11 +332,14 @@ async function main() { } let seed = argv.seed; + if (!seed) { seed = Math.random().toString(); console.log(`No seed supplied, using random seed: ${seed}`); } + const startTime = new Date().getTime(); + if (argv.fleet && !argv.withNewUser) { // warn and exit when using fleet flag console.log( @@ -336,6 +348,29 @@ async function main() { // eslint-disable-next-line no-process-exit process.exit(0); } + + let DocGenerator: typeof EndpointDocGenerator = EndpointDocGenerator; + + // If `--randomVersions` is NOT set, then use custom generator that ensures all data generated + // has a stack version number that matches that of the running stack + if (!argv.randomVersions) { + const stackVersion = await fetchStackVersion(kbnClient); + + // Document Generator override that uses a custom Endpoint Metadata generator and sets the + // `agent.version` to the current version + DocGenerator = class extends EndpointDocGenerator { + constructor(...args: ConstructorParameters<typeof EndpointDocGenerator>) { + const MetadataGenerator = class extends EndpointMetadataGenerator { + protected randomVersion(): string { + return stackVersion; + } + }; + + super(args[0], MetadataGenerator); + } + }; + } + await indexHostsAndAlerts( client, kbnClient, @@ -360,10 +395,11 @@ async function main() { ancestryArraySize: argv.ancestryArraySize, eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(argv.eventIndex), alertsDataStream: EndpointDocGenerator.createDataStreamFromIndex(argv.alertIndex), - } + }, + DocGenerator ); - // delete endpoint_user after + // delete endpoint_user after if (user) { const deleted = await deleteUser(client, user.username); if (deleted.found) { diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 3571ffca63b073..59deaae59f4504 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -22,6 +22,7 @@ import { getPackagePolicyCreateCallback, getPackagePolicyUpdateCallback, getPackagePolicyDeleteCallback, + getPackagePolicyPostCreateCallback, } from '../fleet_integration/fleet_integration'; import type { ManifestManager } from './services/artifacts'; import type { ConfigType } from '../config'; @@ -119,6 +120,11 @@ export class EndpointAppContextService { ) ); + registerIngestCallback( + 'packagePolicyPostCreate', + getPackagePolicyPostCreateCallback(logger, exceptionListsClient) + ); + registerIngestCallback( 'packagePolicyUpdate', getPackagePolicyUpdateCallback( diff --git a/x-pack/plugins/security_solution/server/features.ts b/x-pack/plugins/security_solution/server/features.ts index c962e9fbf2d877..2a794285b52b79 100644 --- a/x-pack/plugins/security_solution/server/features.ts +++ b/x-pack/plugins/security_solution/server/features.ts @@ -45,7 +45,7 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => { ui: casesCapabilities.all, }, read: { - api: ['bulkGetUserProfiles'], + api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles'], app: [CASES_FEATURE_ID, 'kibana'], catalogue: [APP_ID], cases: { diff --git a/x-pack/plugins/security_solution/server/fleet_integration/constants.ts b/x-pack/plugins/security_solution/server/fleet_integration/constants.ts new file mode 100644 index 00000000000000..cefa99722fa3e3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/fleet_integration/constants.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +/** + * Endpoint Security integration presets. + * The default endpoint policy configuration can be overrided based on the preset. + */ + +export const ENDPOINT_CONFIG_PRESET_NGAV = 'NGAV'; +export const ENDPOINT_CONFIG_PRESET_EDR_ESSENTIAL = 'EDREssential'; +export const ENDPOINT_CONFIG_PRESET_EDR_COMPLETE = 'EDRComplete'; + +export const ENDPOINT_INTEGRATION_CONFIG_KEY = 'ENDPOINT_INTEGRATION_CONFIG'; diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index 0c6611acb77e09..664abac92db93e 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts @@ -82,6 +82,7 @@ describe('ingest_integration tests ', () => { enabled: true, streams: [], config: { + integration_config: {}, policy: { value: policyFactory() }, artifact_manifest: { value: manifest }, }, @@ -247,8 +248,13 @@ describe('ingest_integration tests ', () => { expect(manifestManager.pushArtifacts).not.toHaveBeenCalled(); expect(manifestManager.commit).not.toHaveBeenCalled(); }); - }); + it.todo('should override policy config with endpoint settings'); + it.todo('should override policy config with cloud settings'); + }); + describe('package policy post create callback', () => { + it.todo('should create Event Filters given valid parameter on integration config'); + }); describe('package policy update callback (when the license is below platinum)', () => { beforeEach(() => { licenseEmitter.next(Gold); // set license level to gold diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts index e7f21716541cce..f383778b764d2d 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts @@ -12,10 +12,14 @@ import type { PostPackagePolicyCreateCallback, PostPackagePolicyDeleteCallback, PutPackagePolicyUpdateCallback, + PostPackagePolicyPostCreateCallback, } from '@kbn/fleet-plugin/server'; -import type { NewPackagePolicy, UpdatePackagePolicy } from '@kbn/fleet-plugin/common'; - +import type { + NewPackagePolicy, + PackagePolicy, + UpdatePackagePolicy, +} from '@kbn/fleet-plugin/common'; import type { NewPolicyData, PolicyConfig } from '../../common/endpoint/types'; import type { LicenseService } from '../../common/license'; import type { ManifestManager } from '../endpoint/services'; @@ -24,10 +28,14 @@ import { installPrepackagedRules } from './handlers/install_prepackaged_rules'; import { createPolicyArtifactManifest } from './handlers/create_policy_artifact_manifest'; import { createDefaultPolicy } from './handlers/create_default_policy'; import { validatePolicyAgainstLicense } from './handlers/validate_policy_against_license'; +import { validateIntegrationConfig } from './handlers/validate_integration_config'; import { removePolicyFromArtifacts } from './handlers/remove_policy_from_artifacts'; import type { FeatureUsageService } from '../endpoint/services/feature_usage/service'; import type { EndpointMetadataService } from '../endpoint/services/metadata'; import { notifyProtectionFeatureUsage } from './notify_protection_feature_usage'; +import type { AnyPolicyCreateConfig } from './types'; +import { ENDPOINT_INTEGRATION_CONFIG_KEY } from './constants'; +import { createEventFilters } from './handlers/create_event_filters'; const isEndpointPackagePolicy = <T extends { package?: { name: string } }>( packagePolicy: T @@ -56,6 +64,23 @@ export const getPackagePolicyCreateCallback = ( return newPackagePolicy; } + // Optional endpoint integration configuration + let endpointIntegrationConfig; + + // Check if has endpoint integration configuration input + const integrationConfigInput = newPackagePolicy?.inputs?.find( + (input) => input.type === ENDPOINT_INTEGRATION_CONFIG_KEY + )?.config?._config; + + if (integrationConfigInput?.value) { + // The cast below is needed in order to ensure proper typing for the + // Elastic Defend integration configuration + endpointIntegrationConfig = integrationConfigInput.value as AnyPolicyCreateConfig; + + // Validate that the Elastic Defend integration config is valid + validateIntegrationConfig(endpointIntegrationConfig, logger); + } + // In this callback we are handling an HTTP request to the fleet plugin. Since we use // code from the security_solution plugin to handle it (installPrepackagedRules), // we need to build the context that is native to security_solution and pass it there. @@ -81,7 +106,7 @@ export const getPackagePolicyCreateCallback = ( ]); // Add the default endpoint security policy - const defaultPolicyValue = createDefaultPolicy(licenseService); + const defaultPolicyValue = createDefaultPolicy(licenseService, endpointIntegrationConfig); return { // We cast the type here so that any changes to the Endpoint @@ -93,6 +118,9 @@ export const getPackagePolicyCreateCallback = ( enabled: true, streams: [], config: { + integration_config: endpointIntegrationConfig + ? { value: endpointIntegrationConfig } + : {}, artifact_manifest: { value: manifestValue, }, @@ -136,6 +164,30 @@ export const getPackagePolicyUpdateCallback = ( }; }; +export const getPackagePolicyPostCreateCallback = ( + logger: Logger, + exceptionsClient: ExceptionListClient | undefined +): PostPackagePolicyPostCreateCallback => { + return async (packagePolicy: PackagePolicy): Promise<PackagePolicy> => { + // We only care about Endpoint package policies + if (!exceptionsClient || !isEndpointPackagePolicy(packagePolicy)) { + return packagePolicy; + } + + const integrationConfig = packagePolicy?.inputs[0].config?.integration_config; + + if (integrationConfig && integrationConfig?.value?.eventFilters !== undefined) { + createEventFilters( + logger, + exceptionsClient, + integrationConfig.value.eventFilters, + packagePolicy + ); + } + return packagePolicy; + }; +}; + export const getPackagePolicyDeleteCallback = ( exceptionsClient: ExceptionListClient | undefined ): PostPackagePolicyDeleteCallback => { diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts index ab01467e75ceeb..47b4840665e640 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts @@ -11,13 +11,146 @@ import { } from '../../../common/endpoint/models/policy_config'; import type { LicenseService } from '../../../common/license/license'; import { isAtLeast } from '../../../common/license/license'; +import { ProtectionModes } from '../../../common/endpoint/types'; import type { PolicyConfig } from '../../../common/endpoint/types'; +import type { + AnyPolicyCreateConfig, + PolicyCreateCloudConfig, + PolicyCreateEndpointConfig, +} from '../types'; +import { ENDPOINT_CONFIG_PRESET_EDR_ESSENTIAL, ENDPOINT_CONFIG_PRESET_NGAV } from '../constants'; /** - * Create the default endpoint policy based on the current license + * Create the default endpoint policy based on the current license and configuration type */ -export const createDefaultPolicy = (licenseService: LicenseService): PolicyConfig => { - return isAtLeast(licenseService.getLicenseInformation(), 'platinum') +export const createDefaultPolicy = ( + licenseService: LicenseService, + config: AnyPolicyCreateConfig | undefined +): PolicyConfig => { + const policy = isAtLeast(licenseService.getLicenseInformation(), 'platinum') ? policyConfigFactory() : policyConfigFactoryWithoutPaidFeatures(); + + if (config?.type === 'cloud') { + return getCloudPolicyWithIntegrationConfig(policy, config); + } + + return getEndpointPolicyWithIntegrationConfig(policy, config); +}; + +/** + * Set all keys of the given object to false + */ +const falsyObjectKeys = <T extends Record<string, boolean>>(obj: T): T => { + return Object.keys(obj).reduce((accumulator, key) => { + return { ...accumulator, [key]: false }; + }, {} as T); +}; + +/** + * Retrieve policy for endpoint based on the preset selected in the endpoint integration config + */ +const getEndpointPolicyWithIntegrationConfig = ( + policy: PolicyConfig, + config: PolicyCreateEndpointConfig | undefined +): PolicyConfig => { + const isEDREssential = config?.endpointConfig?.preset === ENDPOINT_CONFIG_PRESET_EDR_ESSENTIAL; + + if (config?.endpointConfig?.preset === ENDPOINT_CONFIG_PRESET_NGAV || isEDREssential) { + const events = { + process: true, + file: isEDREssential, + network: isEDREssential, + }; + + return { + ...policy, + linux: { + ...policy.linux, + events: { + ...falsyObjectKeys(policy.linux.events), + ...events, + }, + }, + windows: { + ...policy.windows, + events: { + ...falsyObjectKeys(policy.windows.events), + ...events, + }, + }, + mac: { + ...policy.mac, + events: { + ...falsyObjectKeys(policy.mac.events), + ...events, + }, + }, + }; + } + + return policy; +}; + +/** + * Retrieve policy for cloud based on the on the cloud integration config + */ +const getCloudPolicyWithIntegrationConfig = ( + policy: PolicyConfig, + config: PolicyCreateCloudConfig +): PolicyConfig => { + /** + * Check if the protection is supported, then retrieve Behavior Protection mode based on cloud settings + */ + const getBehaviorProtectionMode = () => { + if (!policy.linux.behavior_protection.supported) { + return ProtectionModes.off; + } + + return config.cloudConfig.preventions.behavior_protection + ? ProtectionModes.prevent + : ProtectionModes.off; + }; + + const protections = { + // Disabling memory_protection, since it's not supported on Cloud integrations + memory_protection: { + supported: false, + mode: ProtectionModes.off, + }, + malware: { + ...policy.linux.malware, + // Malware protection mode based on cloud settings + mode: config.cloudConfig.preventions.malware ? ProtectionModes.prevent : ProtectionModes.off, + }, + behavior_protection: { + ...policy.linux.behavior_protection, + mode: getBehaviorProtectionMode(), + }, + }; + + return { + ...policy, + linux: { + ...policy.linux, + ...protections, + events: { + ...policy.linux.events, + session_data: true, + }, + }, + windows: { + ...policy.windows, + ...protections, + // Disabling ransomware protection, since it's not supported on Cloud integrations + ransomware: { + supported: false, + mode: ProtectionModes.off, + }, + }, + mac: { + ...policy.mac, + ...protections, + }, + }; }; diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_event_filters.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_event_filters.ts new file mode 100644 index 00000000000000..70854e31f69563 --- /dev/null +++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_event_filters.ts @@ -0,0 +1,104 @@ +/* + * 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 uuid from 'uuid'; +import { i18n } from '@kbn/i18n'; +import { + ENDPOINT_EVENT_FILTERS_LIST_ID, + ENDPOINT_EVENT_FILTERS_LIST_NAME, + ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, +} from '@kbn/securitysolution-list-constants'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import { SavedObjectsErrorHelpers } from '@kbn/core/server'; +import type { Logger } from '@kbn/core/server'; +import type { PackagePolicy } from '@kbn/fleet-plugin/common'; +import type { ExceptionListClient } from '@kbn/lists-plugin/server'; +import { wrapErrorIfNeeded } from '../../endpoint/utils'; +import type { PolicyCreateEventFilters } from '../types'; + +const PROCESS_INTERACTIVE_ECS_FIELD = 'process.entry_leader.interactive'; + +/** + * Create the Event Filter list if not exists and Create Event Filters for the Elastic Defend integration. + */ +export const createEventFilters = async ( + logger: Logger, + exceptionsClient: ExceptionListClient, + eventFilters: PolicyCreateEventFilters, + packagePolicy: PackagePolicy +): Promise<void> => { + if (!eventFilters?.nonInteractiveSession) { + return; + } + try { + // Attempt to Create the Event Filter List. It won't create the list if it already exists. + // So we can skip the validation and ignore the conflict error + await exceptionsClient.createExceptionList({ + name: ENDPOINT_EVENT_FILTERS_LIST_NAME, + namespaceType: 'agnostic', + description: ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, + listId: ENDPOINT_EVENT_FILTERS_LIST_ID, + type: ExceptionListTypeEnum.ENDPOINT_EVENTS, + immutable: false, + meta: undefined, + tags: [], + version: 1, + }); + } catch (err) { + // Ignoring error 409 (Conflict) + if (!SavedObjectsErrorHelpers.isConflictError(err)) { + logger.error(`Error creating Event Filter List: ${wrapErrorIfNeeded(err)}`); + return; + } + } + + createNonInteractiveSessionEventFilter(logger, exceptionsClient, packagePolicy); +}; + +/** + * Create an Event Filter for non-interactive sessions and attach it to the policy + */ +export const createNonInteractiveSessionEventFilter = ( + logger: Logger, + exceptionsClient: ExceptionListClient, + packagePolicy: PackagePolicy +): void => { + try { + exceptionsClient.createExceptionListItem({ + listId: ENDPOINT_EVENT_FILTERS_LIST_ID, + description: i18n.translate( + 'xpack.securitySolution.fleetIntegration.elasticDefend.eventFilter.nonInteractiveSessions.description', + { + defaultMessage: 'Event filter for Cloud Security. Created by Elastic Defend integration.', + } + ), + name: i18n.translate( + 'xpack.securitySolution.fleetIntegration.elasticDefend.eventFilter.nonInteractiveSessions.name', + { + defaultMessage: 'Non-interactive Sessions', + } + ), + // Attach to the created policy + tags: [`policy:${packagePolicy.id}`], + osTypes: ['linux'], + type: 'simple', + namespaceType: 'agnostic', + entries: [ + { + field: PROCESS_INTERACTIVE_ECS_FIELD, + operator: 'included', + type: 'match', + value: 'false', + }, + ], + itemId: uuid.v4(), + meta: [], + comments: [], + }); + } catch (err) { + logger.error(`Error creating Event Filter: ${wrapErrorIfNeeded(err)}`); + } +}; diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/validate_integration_config.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/validate_integration_config.ts new file mode 100644 index 00000000000000..d56dd6a02d4cde --- /dev/null +++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/validate_integration_config.ts @@ -0,0 +1,90 @@ +/* + * 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 { Logger } from '@kbn/core/server'; +import { + ENDPOINT_CONFIG_PRESET_EDR_COMPLETE, + ENDPOINT_CONFIG_PRESET_EDR_ESSENTIAL, + ENDPOINT_CONFIG_PRESET_NGAV, +} from '../constants'; +import type { + AnyPolicyCreateConfig, + PolicyCreateCloudConfig, + PolicyCreateEndpointConfig, +} from '../types'; + +// The `statusCode` is used by Fleet API handler to ensure that the proper HTTP code is used in the API response +type THROW_ERROR = Error & { statusCode?: number }; + +const throwError = (message: string): never => { + const error: THROW_ERROR = new Error(message); + error.statusCode = 403; + throw error; +}; + +const validateEndpointIntegrationConfig = ( + config: PolicyCreateEndpointConfig, + logger: Logger +): void => { + if (!config?.endpointConfig?.preset) { + logger.warn('missing endpointConfig preset'); + throwError('invalid endpointConfig preset'); + } + if ( + ![ + ENDPOINT_CONFIG_PRESET_NGAV, + ENDPOINT_CONFIG_PRESET_EDR_COMPLETE, + ENDPOINT_CONFIG_PRESET_EDR_ESSENTIAL, + ].includes(config.endpointConfig.preset) + ) { + logger.warn(`invalid endpointConfig preset: ${config.endpointConfig.preset}`); + throwError('invalid endpointConfig preset'); + } +}; +const validateCloudIntegrationConfig = (config: PolicyCreateCloudConfig, logger: Logger): void => { + if (!config?.cloudConfig?.preventions) { + logger.warn( + 'missing cloudConfig preventions: {preventions : malware: true / false, behavior_protection: true / false}' + ); + throwError('invalid value for cloudConfig: missing preventions '); + } + if (typeof config.cloudConfig.preventions.behavior_protection !== 'boolean') { + logger.warn( + `invalid value for cloudConfig preventions behavior_protection: ${config.cloudConfig.preventions.behavior_protection}` + ); + throwError('invalid value for cloudConfig preventions behavior_protection'); + } + if (typeof config.cloudConfig.preventions.malware !== 'boolean') { + logger.warn( + `invalid value for cloudConfig preventions malware: ${config.cloudConfig.preventions.malware}` + ); + throwError('invalid value for cloudConfig preventions malware'); + } + if (!config?.eventFilters) { + logger.warn( + `eventFilters is required for cloud integration: {eventFilters : nonInteractiveSession: true / false}` + ); + throwError('eventFilters is required for cloud integration'); + } + if (typeof config.eventFilters?.nonInteractiveSession !== 'boolean') { + logger.warn( + `missing or invalid value for eventFilters nonInteractiveSession: ${config.eventFilters?.nonInteractiveSession}` + ); + throwError('invalid value for eventFilters nonInteractiveSession'); + } +}; + +export const validateIntegrationConfig = (config: AnyPolicyCreateConfig, logger: Logger): void => { + if (config.type === 'endpoint') { + validateEndpointIntegrationConfig(config, logger); + } else if (config.type === 'cloud') { + validateCloudIntegrationConfig(config, logger); + } else { + logger.warn(`Invalid integration config type ${config}`); + throwError('Invalid integration config type'); + } +}; diff --git a/x-pack/plugins/security_solution/server/fleet_integration/types.ts b/x-pack/plugins/security_solution/server/fleet_integration/types.ts new file mode 100644 index 00000000000000..2d012832f1ae2f --- /dev/null +++ b/x-pack/plugins/security_solution/server/fleet_integration/types.ts @@ -0,0 +1,30 @@ +/* + * 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 interface PolicyCreateEndpointConfig { + type: 'endpoint'; + endpointConfig: { + preset: 'NGAV' | 'EDREssential' | 'EDRComplete'; + }; +} + +export interface PolicyCreateEventFilters { + nonInteractiveSession?: boolean; +} + +export interface PolicyCreateCloudConfig { + type: 'cloud'; + cloudConfig: { + preventions: { + malware: boolean; + behavior_protection: boolean; + }; + }; + eventFilters?: PolicyCreateEventFilters; +} + +export type AnyPolicyCreateConfig = PolicyCreateEndpointConfig | PolicyCreateCloudConfig; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts index ccd0eb5c80fe6b..f949e927ec0957 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -8,9 +8,9 @@ import { Readable } from 'stream'; import type { HapiReadableStream } from '../../rules/types'; -import type { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock'; +import type { FullResponseSchema } from '../../../../../common/detection_engine/schemas/request'; /** * Given a string, builds a hapi stream as our @@ -34,10 +34,7 @@ export const buildHapiStream = (string: string, filename = 'file.ndjson'): HapiR return stream; }; -export const getOutputRuleAlertForRest = (): Omit< - RulesSchema, - 'machine_learning_job_id' | 'anomaly_threshold' -> => ({ +export const getOutputRuleAlertForRest = (): FullResponseSchema => ({ author: ['Elastic'], actions: [], building_block_type: 'default', @@ -93,4 +90,11 @@ export const getOutputRuleAlertForRest = (): Omit< related_integrations: [], required_fields: [], setup: '', + outcome: undefined, + alias_target_id: undefined, + alias_purpose: undefined, + timestamp_override: undefined, + timestamp_override_fallback_disabled: undefined, + namespace: undefined, + data_view_id: undefined, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts index c5009138b40789..6e76de5aa420dc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -16,7 +16,7 @@ import { readRules } from '../../rules/read_rules'; import { buildSiemResponse } from '../utils'; import { createRulesSchema } from '../../../../../common/detection_engine/schemas/request'; -import { newTransformValidate } from './validate'; +import { transformValidate } from './validate'; import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents'; import { createRules } from '../../rules/create_rules'; import { checkDefaultRuleExceptionListReferences } from './utils/check_for_default_rule_exception_list'; @@ -89,7 +89,7 @@ export const createRulesRoute = ( const ruleExecutionSummary = await ruleExecutionLog.getExecutionSummary(createdRule.id); - const [validated, errors] = newTransformValidate(createdRule, ruleExecutionSummary); + const [validated, errors] = transformValidate(createdRule, ruleExecutionSummary); if (errors != null) { return siemResponse.error({ statusCode: 500, body: errors }); } else { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts index 2df4cb712ddd20..9dfd5b1efed7c0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts @@ -14,7 +14,6 @@ import pMap from 'p-map'; import type { PartialRule, FindResult } from '@kbn/alerting-plugin/server'; import type { ActionsClient, FindActionResult } from '@kbn/actions-plugin/server'; import type { RuleExecutionSummary } from '../../../../../common/detection_engine/rule_monitoring'; -import type { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; import type { ImportRulesSchema } from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; import type { CreateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; import type { RuleAlertType } from '../../rules/types'; @@ -26,6 +25,7 @@ import type { RuleParams } from '../../schemas/rule_schemas'; // eslint-disable-next-line no-restricted-imports import type { LegacyRulesActionsSavedObject } from '../../rule_actions/legacy_get_rule_actions_saved_object'; import type { RuleExecutionSummariesByRuleId } from '../../rule_monitoring'; +import type { FullResponseSchema } from '../../../../../common/detection_engine/schemas/request'; type PromiseFromStreams = ImportRulesSchema | Error; const MAX_CONCURRENT_SEARCHES = 10; @@ -92,7 +92,7 @@ export const getIdBulkError = ({ export const transformAlertsToRules = ( rules: RuleAlertType[], legacyRuleActions: Record<string, LegacyRulesActionsSavedObject> -): Array<Partial<RulesSchema>> => { +): FullResponseSchema[] => { return rules.map((rule) => internalRuleToAPIResponse(rule, null, legacyRuleActions[rule.id])); }; @@ -104,7 +104,7 @@ export const transformFindAlerts = ( page: number; perPage: number; total: number; - data: Array<Partial<RulesSchema>>; + data: Array<Partial<FullResponseSchema>>; } | null => { return { page: ruleFindResults.page, @@ -121,7 +121,7 @@ export const transform = ( rule: PartialRule<RuleParams>, ruleExecutionSummary?: RuleExecutionSummary | null, legacyRuleActions?: LegacyRulesActionsSavedObject | null -): Partial<RulesSchema> | null => { +): FullResponseSchema | null => { if (isAlertType(rule)) { return internalRuleToAPIResponse(rule, ruleExecutionSummary, legacyRuleActions); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts index 21db7e52e4f8d8..84f693a529a68f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts @@ -7,14 +7,14 @@ import { transformValidate, transformValidateBulkError } from './validate'; import type { BulkError } from '../utils'; -import type { RulesSchema } from '../../../../../common/detection_engine/schemas/response'; import { getRuleMock } from '../__mocks__/request_responses'; import { ruleExecutionSummaryMock } from '../../../../../common/detection_engine/rule_monitoring/mocks'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; +import type { FullResponseSchema } from '../../../../../common/detection_engine/schemas/request'; -export const ruleOutput = (): RulesSchema => ({ +export const ruleOutput = (): FullResponseSchema => ({ actions: [], author: ['Elastic'], building_block_type: 'default', @@ -67,6 +67,15 @@ export const ruleOutput = (): RulesSchema => ({ related_integrations: [], required_fields: [], setup: '', + outcome: undefined, + alias_target_id: undefined, + alias_purpose: undefined, + rule_name_override: undefined, + timestamp_override: undefined, + timestamp_override_fallback_disabled: undefined, + namespace: undefined, + data_view_id: undefined, + saved_id: undefined, }); describe('validate', () => { @@ -114,7 +123,7 @@ describe('validate', () => { const rule = getRuleMock(getQueryRuleParams()); const ruleExecutionSumary = ruleExecutionSummaryMock.getSummarySucceeded(); const validatedOrError = transformValidateBulkError('rule-1', rule, ruleExecutionSumary); - const expected: RulesSchema = { + const expected: FullResponseSchema = { ...ruleOutput(), execution_summary: ruleExecutionSumary, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts index 4183f217a61fe9..42e50db79294a8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts @@ -11,8 +11,6 @@ import type { PartialRule } from '@kbn/alerting-plugin/server'; import type { RuleExecutionSummary } from '../../../../../common/detection_engine/rule_monitoring'; import type { FullResponseSchema } from '../../../../../common/detection_engine/schemas/request'; import { fullResponseSchema } from '../../../../../common/detection_engine/schemas/request'; -import type { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; -import { rulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; import { isAlertType } from '../../rules/types'; import type { BulkError } from '../utils'; import { createBulkErrorObject } from '../utils'; @@ -26,19 +24,6 @@ export const transformValidate = ( rule: PartialRule<RuleParams>, ruleExecutionSummary: RuleExecutionSummary | null, legacyRuleActions?: LegacyRulesActionsSavedObject | null -): [RulesSchema | null, string | null] => { - const transformed = transform(rule, ruleExecutionSummary, legacyRuleActions); - if (transformed == null) { - return [null, 'Internal error transforming']; - } else { - return validateNonExact(transformed, rulesSchema); - } -}; - -export const newTransformValidate = ( - rule: PartialRule<RuleParams>, - ruleExecutionSummary: RuleExecutionSummary | null, - legacyRuleActions?: LegacyRulesActionsSavedObject | null ): [FullResponseSchema | null, string | null] => { const transformed = transform(rule, ruleExecutionSummary, legacyRuleActions); if (transformed == null) { @@ -52,10 +37,10 @@ export const transformValidateBulkError = ( ruleId: string, rule: PartialRule<RuleParams>, ruleExecutionSummary: RuleExecutionSummary | null -): RulesSchema | BulkError => { +): FullResponseSchema | BulkError => { if (isAlertType(rule)) { const transformed = internalRuleToAPIResponse(rule, ruleExecutionSummary); - const [validated, errors] = validateNonExact(transformed, rulesSchema); + const [validated, errors] = validateNonExact(transformed, fullResponseSchema); if (errors != null || validated == null) { return createBulkErrorObject({ ruleId, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 31bdfb398c18a5..4fea86a2e3395f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -213,6 +213,13 @@ describe('get_export_by_object_ids', () => { version: 1, exceptions_list: getListArrayMock(), execution_summary: undefined, + outcome: undefined, + alias_target_id: undefined, + alias_purpose: undefined, + timestamp_override: undefined, + timestamp_override_fallback_disabled: undefined, + namespace: undefined, + data_view_id: undefined, }, ], }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts index d5008c87f3b6d3..e044c8fdfd1cab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts @@ -11,7 +11,6 @@ import { transformDataToNdjson } from '@kbn/securitysolution-utils'; import type { Logger } from '@kbn/core/server'; import type { ExceptionListClient } from '@kbn/lists-plugin/server'; import type { RulesClient, RuleExecutorServices } from '@kbn/alerting-plugin/server'; -import type { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { getExportDetailsNdjson } from './get_export_details_ndjson'; @@ -22,10 +21,11 @@ import { getRuleExceptionsForExport } from './get_export_rule_exceptions'; // eslint-disable-next-line no-restricted-imports import { legacyGetBulkRuleActionsSavedObject } from '../rule_actions/legacy_get_bulk_rule_actions_saved_object'; import { internalRuleToAPIResponse } from '../schemas/rule_converters'; +import type { FullResponseSchema } from '../../../../common/detection_engine/schemas/request'; interface ExportSuccessRule { statusCode: 200; - rule: Partial<RulesSchema>; + rule: FullResponseSchema; } interface ExportFailedRule { @@ -36,7 +36,7 @@ interface ExportFailedRule { export interface RulesErrors { exportedCount: number; missingRules: Array<{ rule_id: string }>; - rules: Array<Partial<RulesSchema>>; + rules: FullResponseSchema[]; } export const getExportByObjectIds = async ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.ts index 30be45f5eb163c..204d78f5fe7d2a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.ts @@ -6,12 +6,12 @@ */ import type { ExportExceptionDetails } from '@kbn/securitysolution-io-ts-list-types'; +import type { FullResponseSchema } from '../../../../common/detection_engine/schemas/request'; import type { ExportRulesDetails } from '../../../../common/detection_engine/schemas/response/export_rules_details_schema'; -import type { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; export const getExportDetailsNdjson = ( - rules: Array<Partial<RulesSchema>>, + rules: FullResponseSchema[], missingRules: Array<{ rule_id: string }> = [], exceptionDetails?: ExportExceptionDetails ): string => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts index 38d287754fa953..f01f6820b2687f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts @@ -5,7 +5,6 @@ * 2.0. */ -import type { SavedObjectAttributes } from '@kbn/core/types'; import { BadRequestError } from '@kbn/securitysolution-es-utils'; import { exactCheck, formatErrors } from '@kbn/securitysolution-io-ts-utils'; import { fold } from 'fp-ts/lib/Either'; @@ -56,7 +55,7 @@ export const validateAllPrepackagedRules = ( * Validate the rules from Saved Objects created by Fleet. */ export const validateAllRuleSavedObjects = ( - rules: Array<IRuleAssetSOAttributes & SavedObjectAttributes> + rules: IRuleAssetSOAttributes[] ): AddPrepackagedRulesSchema[] => { return rules.map((rule) => { const decoded = addPrepackagedRulesSchema.decode(rule); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index e02daa7c88c40c..e1b55ca9124b1b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -7,7 +7,7 @@ import type { Readable } from 'stream'; -import type { SavedObjectAttributes, SavedObjectsClientContract } from '@kbn/core/server'; +import type { SavedObjectsClientContract } from '@kbn/core/server'; import type { SanitizedRule } from '@kbn/alerting-plugin/common'; import type { RulesClient, PartialRule } from '@kbn/alerting-plugin/server'; import { ruleTypeMappings } from '@kbn/securitysolution-rules'; @@ -43,7 +43,7 @@ export interface IRuleAssetSOAttributes extends Record<string, any> { export interface IRuleAssetSavedObject { type: string; id: string; - attributes: IRuleAssetSOAttributes & SavedObjectAttributes; + attributes: IRuleAssetSOAttributes; } export interface HapiReadableStream extends Readable { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 6c7d5d581ce613..d3cdcd82b49893 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -497,6 +497,25 @@ export const sampleSignalHit = (): SignalHit => ({ related_integrations: [], required_fields: [], setup: '', + throttle: 'no_actions', + actions: [], + building_block_type: undefined, + note: undefined, + license: undefined, + outcome: undefined, + alias_target_id: undefined, + alias_purpose: undefined, + timeline_id: undefined, + timeline_title: undefined, + meta: undefined, + rule_name_override: undefined, + timestamp_override: undefined, + timestamp_override_fallback_disabled: undefined, + namespace: undefined, + index: undefined, + data_view_id: undefined, + filters: undefined, + saved_id: undefined, }, depth: 1, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index db88284bc88810..5609eed4c0801f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -19,7 +19,6 @@ import type { ListClient } from '@kbn/lists-plugin/server'; import type { EcsFieldMap } from '@kbn/rule-registry-plugin/common/assets/field_maps/ecs_field_map'; import type { TypeOfFieldMap } from '@kbn/rule-registry-plugin/common/field_map'; import type { Status } from '../../../../common/detection_engine/schemas/common/schemas'; -import type { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import type { BaseHit, RuleAlertAction, @@ -42,6 +41,7 @@ import type { WrappedFieldsLatest, } from '../../../../common/detection_engine/schemas/alerts'; import type { IRuleExecutionLogForExecutors } from '../rule_monitoring'; +import type { FullResponseSchema } from '../../../../common/detection_engine/schemas/request'; export interface ThresholdResult { terms?: Array<{ @@ -192,7 +192,7 @@ export interface Signal { _meta?: { version: number; }; - rule: RulesSchema; + rule: FullResponseSchema; /** * @deprecated Use "parents" instead of "parent" */ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.ts index 34280fac17e5e5..bc012953fa0d14 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.ts @@ -258,19 +258,11 @@ export const convertSavedObjectToSavedNote = ( }, identity) ); -// we have to use any here because the SavedObjectAttributes interface is like below -// export interface SavedObjectAttributes { -// [key: string]: SavedObjectAttributes | string | number | boolean | null; -// } -// then this interface does not allow types without index signature -// this is limiting us with our type for now so the easy way was to use any - const pickSavedNote = ( noteId: string | null, savedNote: SavedNote, userInfo: AuthenticatedUser | null - // eslint-disable-next-line @typescript-eslint/no-explicit-any -): any => { +) => { if (noteId == null) { savedNote.created = new Date().valueOf(); savedNote.createdBy = userInfo?.username ?? UNAUTHENTICATED_USER; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/index.ts index ff6fd55bd58929..c6c5f216174ce6 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/index.ts @@ -254,19 +254,11 @@ export const convertSavedObjectToSavedPinnedEvent = ( }, identity) ); -// we have to use any here because the SavedObjectAttributes interface is like below -// export interface SavedObjectAttributes { -// [key: string]: SavedObjectAttributes | string | number | boolean | null; -// } -// then this interface does not allow types without index signature -// this is limiting us with our type for now so the easy way was to use any - export const pickSavedPinnedEvent = ( pinnedEventId: string | null, savedPinnedEvent: SavedPinnedEvent, userInfo: AuthenticatedUser | null - // eslint-disable-next-line @typescript-eslint/no-explicit-any -): any => { +) => { const dateNow = new Date().valueOf(); if (pinnedEventId == null) { savedPinnedEvent.created = dateNow; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.test.ts index b0b2292d6b5f14..d21bc53de178f8 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.test.ts @@ -91,6 +91,12 @@ describe('allHosts search strategy', () => { risk, host: { name: hostName, + risk: { + multipliers: [], + calculated_score_norm: 9999, + calculated_level: risk, + rule_risks: [], + }, }, }, }, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.ts index 57f30ed8703b07..cecfc60fbbaed8 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.ts @@ -18,7 +18,7 @@ import type { HostsEdges, } from '../../../../../../common/search_strategy/security_solution/hosts'; -import type { HostsRiskScore } from '../../../../../../common/search_strategy'; +import type { HostRiskScore } from '../../../../../../common/search_strategy'; import { getHostRiskIndex, buildHostNamesFilter } from '../../../../../../common/search_strategy'; import { inspectStringifyObject } from '../../../../../utils/build_query'; @@ -92,7 +92,7 @@ async function enhanceEdges( const hostsRiskByHostName: Record<string, string> | undefined = hostRiskData?.hits.hits.reduce( (acc, hit) => ({ ...acc, - [hit._source?.host.name ?? '']: hit._source?.risk, + [hit._source?.host.name ?? '']: hit._source?.host.risk.calculated_level, }), {} ); @@ -114,7 +114,7 @@ async function getHostRiskData( hostNames: string[] ) { try { - const hostRiskResponse = await esClient.asCurrentUser.search<HostsRiskScore>( + const hostRiskResponse = await esClient.asCurrentUser.search<HostRiskScore>( buildRiskScoreQuery({ defaultIndex: [getHostRiskIndex(spaceId)], filterQuery: buildHostNamesFilter(hostNames), diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/all/query.risk_score.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/all/query.risk_score.dsl.ts index 069a3e01cdbc1d..d4ec14bb29acfb 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/all/query.risk_score.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/all/query.risk_score.dsl.ts @@ -64,8 +64,12 @@ const getQueryOrder = (sort?: RiskScoreSortField): Sort => { ]; } - if (sort.field === RiskScoreFields.risk) { - return [{ [RiskScoreFields.riskScore]: sort.direction }]; + if (sort.field === RiskScoreFields.hostRisk) { + return [{ [RiskScoreFields.hostRiskScore]: sort.direction }]; + } + + if (sort.field === RiskScoreFields.userRisk) { + return [{ [RiskScoreFields.userRiskScore]: sort.direction }]; } return [{ [sort.field]: sort.direction }]; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/kpi/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/kpi/__mocks__/index.ts index 94830d71e6337e..e494849cc6cebc 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/kpi/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/kpi/__mocks__/index.ts @@ -6,7 +6,7 @@ */ import type { KpiRiskScoreRequestOptions } from '../../../../../../../common/search_strategy'; -import { RiskQueries } from '../../../../../../../common/search_strategy'; +import { RiskScoreEntity, RiskQueries } from '../../../../../../../common/search_strategy'; export const mockOptions: KpiRiskScoreRequestOptions = { defaultIndex: [ @@ -22,5 +22,5 @@ export const mockOptions: KpiRiskScoreRequestOptions = { factoryQueryType: RiskQueries.kpiRiskScore, filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}},{"bool":{"filter":[{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}', - aggBy: 'host.name', + entity: RiskScoreEntity.host, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/kpi/query.kpi_risk_score.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/kpi/query.kpi_risk_score.dsl.ts index ace0cece7c9814..f68eb647ad88c6 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/kpi/query.kpi_risk_score.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/risk_score/kpi/query.kpi_risk_score.dsl.ts @@ -5,13 +5,14 @@ * 2.0. */ +import { RiskScoreEntity, RiskScoreFields } from '../../../../../../common/search_strategy'; import type { KpiRiskScoreRequestOptions } from '../../../../../../common/search_strategy'; import { createQueryFilterClauses } from '../../../../../utils/build_query'; export const buildKpiRiskScoreQuery = ({ defaultIndex, filterQuery, - aggBy, + entity, }: KpiRiskScoreRequestOptions) => { const filter = [...createQueryFilterClauses(filterQuery)]; @@ -24,12 +25,16 @@ export const buildKpiRiskScoreQuery = ({ aggs: { risk: { terms: { - field: 'risk.keyword', + field: + entity === RiskScoreEntity.user ? RiskScoreFields.userRisk : RiskScoreFields.hostRisk, }, aggs: { unique_entries: { cardinality: { - field: aggBy, + field: + entity === RiskScoreEntity.user + ? RiskScoreFields.userName + : RiskScoreFields.hostName, }, }, }, diff --git a/x-pack/plugins/session_view/common/types/aggregate/index.ts b/x-pack/plugins/session_view/common/types/aggregate/index.ts new file mode 100644 index 00000000000000..7080bf1af9bd7a --- /dev/null +++ b/x-pack/plugins/session_view/common/types/aggregate/index.ts @@ -0,0 +1,11 @@ +/* + * 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 interface Aggregate { + key: string | number; + doc_count: number; +} diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/buttons.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/buttons.tsx index 9cc842aac76685..cbd4f7695c36ac 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/buttons.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/buttons.tsx @@ -64,3 +64,26 @@ export const AlertButton = ({ </EuiButton> ); }; + +export const OutputButton = ({ + isExpanded, + onToggle, +}: { + isExpanded: boolean; + onToggle: () => void; +}) => { + const { outputButton, buttonArrow } = useButtonStyles(); + + return ( + <EuiButton + className={isExpanded ? 'isExpanded' : ''} + key="output-button" + css={outputButton} + onClick={onToggle} + data-test-subj="processTreeNodeOutpuButton" + > + <FormattedMessage id="xpack.sessionView.output" defaultMessage="Output" /> + <EuiIcon css={buttonArrow} size="s" type="arrowDown" /> + </EuiButton> + ); +}; diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx index 857c791d81a3a8..a78778d3f6368a 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx @@ -254,6 +254,23 @@ describe('ProcessTreeNode component', () => { expect(renderResult.queryByTestId('sessionView:sessionViewAlertDetails')).toBeFalsy(); }); }); + + describe('Output', () => { + it('renders Output button when process has output', async () => { + const processMockWithOutput = { + ...sessionViewAlertProcessMock, + hasOutput: () => true, + }; + renderResult = mockedContext.render( + <ProcessTreeNode {...props} process={processMockWithOutput} /> + ); + + expect(renderResult.queryByTestId('processTreeNodeOutpuButton')).toBeTruthy(); + expect(renderResult.queryByTestId('processTreeNodeOutpuButton')?.textContent).toBe( + 'Output' + ); + }); + }); describe('Child processes', () => { it('renders Child processes button when process has Child processes', async () => { const processMockWithChildren: typeof processMock = { diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx index 47f56b6c51a42e..94518993255ee9 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx @@ -27,7 +27,7 @@ import { Process } from '../../../common/types/process_tree'; import { dataOrDash } from '../../utils/data_or_dash'; import { useVisible } from '../../hooks/use_visible'; import { ProcessTreeAlerts } from '../process_tree_alerts'; -import { AlertButton, ChildrenProcessesButton } from './buttons'; +import { AlertButton, ChildrenProcessesButton, OutputButton } from './buttons'; import { useButtonStyles } from './use_button_styles'; import { useStyles } from './styles'; import { SplitText } from './split_text'; @@ -75,6 +75,7 @@ export function ProcessTreeNode({ }: ProcessDeps) { const [childrenExpanded, setChildrenExpanded] = useState(isSessionLeader || process.autoExpand); const [alertsExpanded, setAlertsExpanded] = useState(false); + const [outputExpanded, setOutputExpanded] = useState(false); const { searchMatched } = process; const dateFormat = useDateFormat(); @@ -94,6 +95,7 @@ export function ProcessTreeNode({ const alerts = process.getAlerts(); const hasAlerts = useMemo(() => !!alerts.length, [alerts]); + const hasOutputs = useMemo(() => process.hasOutput(), [process]); const hasInvestigatedAlert = useMemo( () => !!( @@ -140,6 +142,10 @@ export function ProcessTreeNode({ setAlertsExpanded(!alertsExpanded); }, [alertsExpanded]); + const onOutputToggle = useCallback(() => { + setOutputExpanded(!outputExpanded); + }, [outputExpanded]); + const onProcessClicked = useCallback( (e: MouseEvent) => { e.stopPropagation(); @@ -286,13 +292,14 @@ export function ProcessTreeNode({ {!isSessionLeader && children.length > 0 && ( <ChildrenProcessesButton isExpanded={childrenExpanded} onToggle={onChildrenToggle} /> )} - {alerts.length > 0 && ( + {hasAlerts && ( <AlertButton onToggle={onAlertsToggle} isExpanded={alertsExpanded} alertsCount={alerts.length} /> )} + {hasOutputs && <OutputButton onToggle={onOutputToggle} isExpanded={outputExpanded} />} </div> </div> diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts b/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts index 8cd4e2c5004b4c..38081148e2c5d6 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts +++ b/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts @@ -74,6 +74,24 @@ export const useButtonStyles = () => { }, }; + const outputButton: CSSObject = { + ...button, + color: euiVars.euiColorVis1, + background: transparentize(euiVars.euiColorVis1, 0.04), + border: `${border.width.thin} solid ${transparentize(euiVars.euiColorVis1, 0.48)}`, + '&&:hover, &&:focus': { + background: transparentize(euiVars.euiColorVis1, 0.12), + textDecoration: 'none', + }, + '&.isExpanded': { + color: colors.ghost, + background: euiVars.euiColorVis1, + '&:hover, &:focus': { + background: `${euiVars.euiColorVis1}`, + }, + }, + }; + const userChangedButton: CSSObject = { ...button, cursor: 'default', @@ -97,6 +115,7 @@ export const useButtonStyles = () => { buttonArrow, button, alertButton, + outputButton, userChangedButton, buttonSize, }; diff --git a/x-pack/plugins/session_view/server/routes/io_events_route.test.ts b/x-pack/plugins/session_view/server/routes/io_events_route.test.ts new file mode 100644 index 00000000000000..5ad0615665c64d --- /dev/null +++ b/x-pack/plugins/session_view/server/routes/io_events_route.test.ts @@ -0,0 +1,77 @@ +/* + * 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 { elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { EventAction, EventKind } from '../../common/types/process_tree'; +import { searchProcessWithIOEvents } from './io_events_route'; + +const getEmptyResponse = async () => { + return { + aggregations: { + custom_agg: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [], + }, + }, + }; +}; + +const getResponse = async () => { + return { + aggregations: { + custom_agg: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'mockId', + doc_count: 1, + }, + ], + }, + }, + }; +}; + +describe('io_events_route.ts', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('searchProcessWithIOEvents(client, sessionEntityId, range)', () => { + it('should return an empty events array for a non existant entity_id', async () => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(getEmptyResponse()); + const body = await searchProcessWithIOEvents(esClient, 'asdf'); + + expect(body.length).toBe(0); + }); + + it('returns results for a particular session entity_id', async () => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(getResponse()); + + const body = await searchProcessWithIOEvents(esClient, 'mockId'); + + expect(body.length).toBe(1); + + body.forEach((event) => { + expect(event._source?.event?.action).toBe(EventAction.text_output); + expect(event._source?.event?.kind).toBe(EventKind.event); + expect(event._source?.process?.entity_id).toBe('mockId'); + }); + }); + + it('takes a range', async () => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(getResponse()); + + const start = '2021-11-23T15:25:04.210Z'; + const end = '2021-20-23T15:25:04.210Z'; + const body = await searchProcessWithIOEvents(esClient, 'mockId', [start, end]); + + expect(body.length).toBe(1); + }); + }); +}); 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 5ba61030aad9cc..327391f9aab21d 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 @@ -8,7 +8,8 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '@kbn/core/server'; import { EVENT_ACTION, TIMESTAMP } from '@kbn/rule-data-utils'; import type { ElasticsearchClient } from '@kbn/core/server'; -import { ProcessEvent } from '../../common/types/process_tree'; +import { Aggregate } from '../../common/types/aggregate'; +import { EventAction, EventKind, ProcessEvent } from '../../common/types/process_tree'; import { IO_EVENTS_ROUTE, IO_EVENTS_PER_PAGE, @@ -17,6 +18,8 @@ import { TTY_CHAR_DEVICE_MAJOR_PROPERTY, TTY_CHAR_DEVICE_MINOR_PROPERTY, HOST_BOOT_ID_PROPERTY, + PROCESS_ENTITY_ID_PROPERTY, + PROCESS_EVENTS_PER_PAGE, } from '../../common/constants'; /** @@ -135,3 +138,67 @@ export const registerIOEventsRoute = (router: IRouter) => { } ); }; + +export const searchProcessWithIOEvents = async ( + client: ElasticsearchClient, + sessionEntityId: string, + range?: string[] +) => { + const rangeFilter = range + ? [ + { + range: { + '@timestamp': { + gte: range[0], + lte: range[1], + }, + }, + }, + ] + : []; + + const search = await client.search({ + index: [PROCESS_EVENTS_INDEX], + body: { + query: { + bool: { + must: [ + { term: { [EVENT_ACTION]: 'text_output' } }, + { term: { [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId } }, + ...rangeFilter, + ], + }, + }, + size: 0, + aggs: { + custom_agg: { + terms: { + field: PROCESS_ENTITY_ID_PROPERTY, + }, + aggs: { + bucket_sort: { + bucket_sort: { + size: PROCESS_EVENTS_PER_PAGE, + }, + }, + }, + }, + }, + }, + }); + + const agg: any = search.aggregations?.custom_agg; + const buckets: Aggregate[] = agg?.buckets || []; + + return buckets.map((bucket) => ({ + _source: { + event: { + kind: EventKind.event, + action: EventAction.text_output, + }, + process: { + entity_id: bucket.key, + }, + }, + })); +}; 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 2eeb29cd467496..8c56c305d6774b 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 @@ -22,6 +22,7 @@ import { } from '../../common/constants'; import { ProcessEvent } from '../../common/types/process_tree'; import { searchAlerts } from './alerts_route'; +import { searchProcessWithIOEvents } from './io_events_route'; export const registerProcessEventsRoute = ( router: IRouter, @@ -127,7 +128,9 @@ export const fetchEventsAndScopedAlerts = async ( range ); - events = [...events, ...alertsBody.events]; + const processesWithIOEvents = await searchProcessWithIOEvents(client, sessionEntityId, range); + + events = [...events, ...alertsBody.events, ...processesWithIOEvents]; } return { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx index b3d7f10c2b7b53..1220c50456bb48 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx @@ -411,6 +411,28 @@ export const FIELD: Record<string, FieldMeta> = { }; }, }, + [ConfigKey.ENABLED]: { + fieldKey: ConfigKey.ENABLED, + component: EuiSwitch, + label: i18n.translate('xpack.synthetics.monitorConfig.enabled.label', { + defaultMessage: 'Enable Monitor', + }), + controlled: true, + props: ({ isEdit, setValue }) => ({ + id: 'syntheticsMontiorConfigIsEnabled', + label: isEdit + ? i18n.translate('xpack.synthetics.monitorConfig.edit.enabled.label', { + defaultMessage: 'Disabled monitors do not run tests.', + }) + : i18n.translate('xpack.synthetics.monitorConfig.create.enabled.label', { + defaultMessage: + 'Disabled monitors do not run tests. You can create a disabled monitor and enable it later.', + }), + onChange: (event: React.ChangeEvent<HTMLInputElement>) => { + setValue(ConfigKey.ENABLED, !!event.target.checked); + }, + }), + }, [ConfigKey.TAGS]: { fieldKey: ConfigKey.TAGS, component: ComboBox, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx index 1c49665af081eb..9f11f2c53c06e5 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx @@ -149,6 +149,7 @@ export const FORM_CONFIG: FieldConfig = { FIELD[ConfigKey.SCHEDULE], FIELD[ConfigKey.MAX_REDIRECTS], FIELD[ConfigKey.TIMEOUT], + FIELD[ConfigKey.ENABLED], ], advanced: [ DEFAULT_DATA_OPTIONS, @@ -166,6 +167,7 @@ export const FORM_CONFIG: FieldConfig = { FIELD[ConfigKey.LOCATIONS], FIELD[ConfigKey.SCHEDULE], FIELD[ConfigKey.TIMEOUT], + FIELD[ConfigKey.ENABLED], ], advanced: [ DEFAULT_DATA_OPTIONS, @@ -181,6 +183,7 @@ export const FORM_CONFIG: FieldConfig = { FIELD[ConfigKey.LOCATIONS], FIELD[ConfigKey.SCHEDULE], FIELD[ConfigKey.THROTTLING_CONFIG], + FIELD[ConfigKey.ENABLED], ], step3: [FIELD[ConfigKey.SOURCE_INLINE]], scriptEdit: [FIELD[ConfigKey.SOURCE_INLINE]], @@ -205,6 +208,7 @@ export const FORM_CONFIG: FieldConfig = { FIELD[ConfigKey.LOCATIONS], FIELD[ConfigKey.SCHEDULE], FIELD[ConfigKey.THROTTLING_CONFIG], + FIELD[ConfigKey.ENABLED], ], advanced: [ { @@ -227,6 +231,7 @@ export const FORM_CONFIG: FieldConfig = { FIELD[ConfigKey.SCHEDULE], FIELD[ConfigKey.WAIT], FIELD[ConfigKey.TIMEOUT], + FIELD[ConfigKey.ENABLED], ], advanced: [DEFAULT_DATA_OPTIONS], }, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.test.tsx index 80b34b47fdc9f1..1cf2baf1df94f1 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.test.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.test.tsx @@ -8,81 +8,85 @@ import { format } from './formatter'; describe('format', () => { - const formValues = { - type: 'http', - form_monitor_type: 'http', - enabled: true, - schedule: { - number: '3', - unit: 'm', - }, - 'service.name': '', - tags: [], - timeout: '16', - name: 'Sample name', - locations: [ - { - id: 'us_central', - isServiceManaged: true, + let formValues: Record<string, unknown>; + beforeEach(() => { + formValues = { + type: 'http', + form_monitor_type: 'http', + enabled: true, + schedule: { + number: '3', + unit: 'm', }, - ], - namespace: 'default', - origin: 'ui', - __ui: { - is_tls_enabled: false, - }, - urls: 'sample url', - max_redirects: '0', - password: '', - proxy_url: '', - 'check.response.body.negative': [], - 'check.response.body.positive': [], - 'response.include_body': 'on_error', - 'check.response.headers': {}, - 'response.include_headers': true, - 'check.response.status': [], - 'check.request.body': { - value: '', - type: 'text', - }, - 'check.request.headers': {}, - 'check.request.method': 'GET', - username: '', - 'ssl.certificate_authorities': '', - 'ssl.certificate': '', - 'ssl.key': '', - 'ssl.key_passphrase': '', - 'ssl.verification_mode': 'full', - 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], - isTLSEnabled: false, - service: { - name: '', - }, - check: { - request: { - method: 'GET', - headers: {}, - body: { - type: 'text', - value: '', + 'service.name': '', + tags: [], + timeout: '16', + name: 'Sample name', + locations: [ + { + id: 'us_central', + isServiceManaged: true, }, + ], + namespace: 'default', + origin: 'ui', + __ui: { + is_tls_enabled: false, }, - response: { - status: [], - headers: {}, - body: { - positive: [], - negative: [], + urls: 'sample url', + max_redirects: '0', + password: '', + proxy_url: '', + 'check.response.body.negative': [], + 'check.response.body.positive': [], + 'response.include_body': 'on_error', + 'check.response.headers': {}, + 'response.include_headers': true, + 'check.response.status': [], + 'check.request.body': { + value: '', + type: 'text', + }, + 'check.request.headers': {}, + 'check.request.method': 'GET', + username: '', + 'ssl.certificate_authorities': '', + 'ssl.certificate': '', + 'ssl.key': '', + 'ssl.key_passphrase': '', + 'ssl.verification_mode': 'full', + 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], + isTLSEnabled: false, + service: { + name: '', + }, + check: { + request: { + method: 'GET', + headers: {}, + body: { + type: 'text', + value: '', + }, }, + response: { + status: [], + headers: {}, + body: { + positive: [], + negative: [], + }, + }, + }, + response: { + include_headers: true, + include_body: 'on_error', }, - }, - response: { - include_headers: true, - include_body: 'on_error', - }, - }; + }; + }); - it('correctly formats form fields to monitor type', () => { + it.each([[true], [false]])('correctly formats form fields to monitor type', (enabled) => { + formValues.enabled = enabled; expect(format(formValues)).toEqual({ __ui: { is_tls_enabled: false, @@ -98,7 +102,7 @@ describe('format', () => { 'check.response.body.positive': [], 'check.response.headers': {}, 'check.response.status': [], - enabled: true, + enabled, form_monitor_type: 'http', locations: [ { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.ts index 8da53f6cbb2806..3495e0104e852c 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.ts @@ -15,7 +15,7 @@ export const formatter = (fields: Record<string, any>) => { Object.keys(defaults).map((key) => { /* split key names on dot to handle dot notation fields, * which are changed to nested fields by react-hook-form */ - monitorFields[key] = get(fields, key.split('.')) || defaults[key as ConfigKey]; + monitorFields[key] = get(fields, key.split('.')) ?? defaults[key as ConfigKey]; }); return monitorFields as MonitorFields; }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/monitor_summary_title.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/monitor_summary_title.tsx index d8c8e4394f3a56..9e295496dd77c0 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/monitor_summary_title.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/monitor_summary_title.tsx @@ -10,6 +10,7 @@ import { useParams } from 'react-router-dom'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { MonitorSummaryLastRunInfo } from './last_run_info'; import { getMonitorStatusAction, selectMonitorStatus } from '../../state'; +import { RunTestManually } from './run_test_manually'; export const MonitorSummaryTitle = () => { const dispatch = useDispatch(); @@ -23,9 +24,18 @@ export const MonitorSummaryTitle = () => { }, [dispatch, monitorId]); return ( - <EuiFlexGroup direction="column" gutterSize="xs"> - <EuiFlexItem>{data?.monitor.name}</EuiFlexItem> - <EuiFlexItem grow={false}>{data && <MonitorSummaryLastRunInfo ping={data} />}</EuiFlexItem> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFlexGroup direction="column" gutterSize="xs"> + <EuiFlexItem>{data?.monitor.name}</EuiFlexItem> + <EuiFlexItem grow={false}> + {data && <MonitorSummaryLastRunInfo ping={data} />} + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <RunTestManually /> + </EuiFlexItem> </EuiFlexGroup> ); }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx index 12783d39e27112..8da37518fbede2 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx @@ -17,7 +17,6 @@ import { useInspectorContext } from '@kbn/observability-plugin/public'; import type { LazyObservabilityPageTemplateProps } from '@kbn/observability-plugin/public'; import { MonitorAddPage } from './components/monitor_add_edit/monitor_add_page'; import { MonitorEditPage } from './components/monitor_add_edit/monitor_edit_page'; -import { RunTestManually } from './components/monitor_summary/run_test_manually'; import { MonitorSummaryHeaderContent } from './components/monitor_summary/monitor_summary_header_content'; import { MonitorSummaryTitle } from './components/monitor_summary/monitor_summary_title'; import { MonitorSummaryPage } from './components/monitor_summary/monitor_summary'; @@ -79,22 +78,17 @@ const getRoutes = ( }, }, { - title: i18n.translate('xpack.synthetics.gettingStartedRoute.title', { - defaultMessage: 'Synthetics Getting Started | {baseTitle}', + title: i18n.translate('xpack.synthetics.monitorSummaryRoute.title', { + defaultMessage: 'Monitor summary | {baseTitle}', values: { baseTitle }, }), path: MONITOR_ROUTE, component: () => <MonitorSummaryPage />, dataTestSubj: 'syntheticsGettingStartedPage', - pageSectionProps: { - alignment: 'center', - paddingSize: 'none', - }, pageHeader: { - paddingSize: 'none', children: <MonitorSummaryHeaderContent />, pageTitle: <MonitorSummaryTitle />, - rightSideItems: [<RunTestManually />], + // rightSideItems: [<RunTestManually />], }, }, { diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/pages/overview.test.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/pages/overview.test.tsx index f3808984137df4..b3aa4714fa6647 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/pages/overview.test.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/pages/overview.test.tsx @@ -8,15 +8,8 @@ import React from 'react'; import { OverviewPageComponent } from './overview'; import { render } from '../lib/helper/rtl_helpers'; -import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; -import { setAutocomplete } from '@kbn/unified-search-plugin/public/services'; describe('MonitorPage', () => { - beforeEach(() => { - const autocompleteStart = unifiedSearchPluginMock.createStartContract(); - setAutocomplete(autocompleteStart.autocomplete); - }); - it('renders expected elements for valid props', async () => { const { findByText, findByPlaceholderText } = render(<OverviewPageComponent />); diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts index 832c4322a28668..12b958fa3c67d4 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts @@ -20,7 +20,6 @@ import { SyntheticsMonitorWithSecrets, SyntheticsMonitor, ConfigKey, - FormMonitorType, } from '../../../common/runtime_types'; import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types'; import { API_URLS } from '../../../common/constants'; @@ -146,10 +145,7 @@ export const syncEditedMonitor = async ({ const editedSOPromise = savedObjectsClient.update<MonitorFields>( syntheticsMonitorType, previousMonitor.id, - monitorWithRevision.type === 'browser' && - monitorWithRevision[ConfigKey.FORM_MONITOR_TYPE] !== FormMonitorType.SINGLE - ? { ...monitorWithRevision, urls: '' } - : monitorWithRevision + monitorWithRevision ); const editSyncPromise = syntheticsMonitorClient.editMonitor( diff --git a/x-pack/plugins/synthetics/server/synthetics_service/hydrate_saved_object.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/hydrate_saved_object.test.ts deleted file mode 100644 index c2c0412d940c5d..00000000000000 --- a/x-pack/plugins/synthetics/server/synthetics_service/hydrate_saved_object.test.ts +++ /dev/null @@ -1,125 +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 { hydrateSavedObjects } from './hydrate_saved_object'; -import { DecryptedSyntheticsMonitorSavedObject } from '../../common/types'; -import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; -import { getUptimeESMockClient } from '../legacy_uptime/lib/requests/test_helpers'; -import { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; - -import moment from 'moment'; - -describe('hydrateSavedObjects', () => { - const { uptimeEsClient: mockUptimeEsClient, esClient: mockEsClient } = getUptimeESMockClient(); - - const mockMonitorTemplate = { - id: 'my-mock-monitor', - attributes: { - type: 'browser', - name: 'Test Browser Monitor 01', - }, - }; - - const serverMock: UptimeServerSetup = { - uptimeEsClient: mockUptimeEsClient, - authSavedObjectsClient: { - bulkUpdate: jest.fn(), - }, - } as unknown as UptimeServerSetup; - - const toKibanaResponse = (hits: Array<{ _source: Record<string, string | object> }>) => ({ - body: { hits: { hits } }, - }); - - beforeEach(() => { - mockUptimeEsClient.baseESClient.security.hasPrivileges = jest - .fn() - .mockResolvedValue({ has_all_requested: true }); - }); - - it.each([['browser'], ['http'], ['tcp']])( - 'hydrates missing data for %s monitors', - async (type) => { - const time = moment(); - const monitor = { - ...mockMonitorTemplate, - attributes: { ...mockMonitorTemplate.attributes, type }, - updated_at: moment(time).subtract(1, 'hour').toISOString(), - } as DecryptedSyntheticsMonitorSavedObject; - - const monitors: DecryptedSyntheticsMonitorSavedObject[] = [monitor]; - - mockEsClient.search.mockResolvedValue( - toKibanaResponse([ - { - _source: { - config_id: monitor.id, - '@timestamp': moment(time).toISOString(), - url: { port: 443, full: 'https://example.com' }, - }, - }, - ]) as unknown as SearchResponse - ); - - await hydrateSavedObjects({ monitors, server: serverMock }); - - expect(serverMock.authSavedObjectsClient?.bulkUpdate).toHaveBeenCalledWith([ - { - ...monitor, - attributes: { - ...monitor.attributes, - 'url.port': 443, - urls: 'https://example.com', - }, - }, - ]); - } - ); - - it.each([['browser'], ['http'], ['tcp']])( - 'does not hydrate when the user does not have permissions', - async (type) => { - const time = moment(); - const monitor = { - ...mockMonitorTemplate, - attributes: { ...mockMonitorTemplate.attributes, type }, - updated_at: moment(time).subtract(1, 'hour').toISOString(), - } as DecryptedSyntheticsMonitorSavedObject; - - const monitors: DecryptedSyntheticsMonitorSavedObject[] = [monitor]; - - mockUptimeEsClient.baseESClient.security.hasPrivileges = jest - .fn() - .mockResolvedValue({ has_all_requested: false }); - - mockEsClient.search.mockResolvedValue( - toKibanaResponse([ - { - _source: { - config_id: monitor.id, - '@timestamp': moment(time).toISOString(), - url: { port: 443, full: 'https://example.com' }, - }, - }, - ]) as unknown as SearchResponse - ); - - await hydrateSavedObjects({ monitors, server: serverMock }); - - expect(serverMock.authSavedObjectsClient?.bulkUpdate).not.toHaveBeenCalledWith([ - { - ...monitor, - attributes: { - ...monitor.attributes, - 'url.port': 443, - urls: 'https://example.com', - }, - }, - ]); - } - ); -}); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/hydrate_saved_object.ts b/x-pack/plugins/synthetics/server/synthetics_service/hydrate_saved_object.ts deleted file mode 100644 index a2ea7a906fb7b6..00000000000000 --- a/x-pack/plugins/synthetics/server/synthetics_service/hydrate_saved_object.ts +++ /dev/null @@ -1,156 +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 moment from 'moment'; -import type { SecurityIndexPrivilege } from '@elastic/elasticsearch/lib/api/types'; -import { UptimeESClient } from '../legacy_uptime/lib/lib'; -import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; -import { DecryptedSyntheticsMonitorSavedObject } from '../../common/types'; -import { SyntheticsMonitor, MonitorFields, Ping } from '../../common/runtime_types'; -import { SYNTHETICS_INDEX_PATTERN } from '../../common/constants'; - -export const hydrateSavedObjects = async ({ - monitors, - server, -}: { - monitors: DecryptedSyntheticsMonitorSavedObject[]; - server: UptimeServerSetup; -}) => { - try { - const { uptimeEsClient } = server; - if (!uptimeEsClient) { - return; - } - - const { has_all_requested: hasAllPrivileges } = - await uptimeEsClient.baseESClient.security.hasPrivileges({ - body: { - index: [ - { - names: ['synthetics-*'], - privileges: ['read'] as SecurityIndexPrivilege[], - }, - ], - }, - }); - - if (!hasAllPrivileges) { - return; - } - - const missingInfoIds: string[] = monitors - .filter((monitor) => { - const isBrowserMonitor = monitor.attributes.type === 'browser'; - const isHTTPMonitor = monitor.attributes.type === 'http'; - const isTCPMonitor = monitor.attributes.type === 'tcp'; - - const monitorAttributes = monitor.attributes as MonitorFields; - const isMissingUrls = !monitorAttributes || !monitorAttributes.urls; - const isMissingPort = !monitorAttributes || !monitorAttributes['url.port']; - - const isEnrichableBrowserMonitor = isBrowserMonitor && (isMissingUrls || isMissingPort); - const isEnrichableHttpMonitor = isHTTPMonitor && isMissingPort; - const isEnrichableTcpMonitor = isTCPMonitor && isMissingPort; - - return isEnrichableBrowserMonitor || isEnrichableHttpMonitor || isEnrichableTcpMonitor; - }) - .map(({ id }) => id); - - if (missingInfoIds.length > 0 && server.uptimeEsClient) { - const esDocs: Ping[] = await fetchSampleMonitorDocuments( - server.uptimeEsClient, - missingInfoIds - ); - - const updatedObjects: DecryptedSyntheticsMonitorSavedObject[] = []; - monitors - .filter((monitor) => missingInfoIds.includes(monitor.id)) - .forEach((monitor) => { - let resultAttributes: SyntheticsMonitor = monitor.attributes; - - let isUpdated = false; - - esDocs.forEach((doc) => { - // to make sure the document is ingested after the latest update of the monitor - const documentIsAfterLatestUpdate = moment(monitor.updated_at).isBefore( - moment(doc.timestamp) - ); - if (!documentIsAfterLatestUpdate) return monitor; - if (doc.config_id !== monitor.id) return monitor; - - if (doc.url?.full) { - isUpdated = true; - resultAttributes = { ...resultAttributes, urls: doc.url?.full }; - } - - if (doc.url?.port) { - isUpdated = true; - resultAttributes = { ...resultAttributes, ['url.port']: doc.url?.port }; - } - }); - if (isUpdated) { - updatedObjects.push({ - ...monitor, - attributes: resultAttributes, - } as DecryptedSyntheticsMonitorSavedObject); - } - }); - - await server.authSavedObjectsClient?.bulkUpdate(updatedObjects); - } - } catch (e) { - server.logger.error(e); - } -}; - -const fetchSampleMonitorDocuments = async (esClient: UptimeESClient, configIds: string[]) => { - const data = await esClient.search( - { - body: { - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: 'now-15m', - lt: 'now', - }, - }, - }, - { - terms: { - config_id: configIds, - }, - }, - { - exists: { - field: 'summary', - }, - }, - { - bool: { - minimum_should_match: 1, - should: [{ exists: { field: 'url.full' } }, { exists: { field: 'url.port' } }], - }, - }, - ], - }, - }, - _source: ['url', 'config_id', '@timestamp'], - collapse: { - field: 'config_id', - }, - }, - }, - 'getHydrateQuery', - SYNTHETICS_INDEX_PATTERN - ); - return data.body.hits.hits.map( - ({ _source: doc }) => ({ ...(doc as any), timestamp: (doc as any)['@timestamp'] } as Ping) - ); -}; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor_formatter.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor_formatter.ts index c96b06886a3060..b9fe8fafdfc089 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor_formatter.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor_formatter.ts @@ -258,15 +258,14 @@ export class ProjectMonitorFormatter { attributes: { [ConfigKey.REVISION]: _, ...normalizedPreviousMonitorAttributes }, } = normalizeSecrets(decryptedPreviousMonitor); const hasMonitorBeenEdited = !isEqual(normalizedMonitor, normalizedPreviousMonitorAttributes); - const monitorWithRevision = formatSecrets({ - ...normalizedPreviousMonitorAttributes, // ensures monitor AAD remains consistent in the event of field name changes - ...normalizedMonitor, - revision: hasMonitorBeenEdited - ? (previousMonitor.attributes[ConfigKey.REVISION] || 0) + 1 - : previousMonitor.attributes[ConfigKey.REVISION], - }); if (hasMonitorBeenEdited) { + const monitorWithRevision = formatSecrets({ + ...normalizedPreviousMonitorAttributes, + ...normalizedMonitor, + revision: (previousMonitor.attributes[ConfigKey.REVISION] || 0) + 1, + }); + const { editedMonitor } = await syncEditedMonitor({ normalizedMonitor, monitorWithRevision, diff --git a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts index 55c0b7f16e14af..60c98bd5cc1c9d 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts @@ -42,8 +42,6 @@ import { HeartbeatConfig, } from '../../common/runtime_types'; import { getServiceLocations } from './get_service_locations'; -import { hydrateSavedObjects } from './hydrate_saved_object'; -import { DecryptedSyntheticsMonitorSavedObject } from '../../common/types'; import { normalizeSecrets } from './utils/secrets'; @@ -434,17 +432,36 @@ export class SyntheticsService { const start = performance.now(); - const monitors: Array<SavedObject<SyntheticsMonitorWithSecrets>> = await Promise.all( - encryptedMonitors.map((monitor) => - encryptedClient.getDecryptedAsInternalUser<SyntheticsMonitorWithSecrets>( - syntheticsMonitor.name, - monitor.id, - { - namespace: monitor.namespaces?.[0], - } + const monitors: Array<SavedObject<SyntheticsMonitorWithSecrets>> = ( + await Promise.all( + encryptedMonitors.map( + (monitor) => + new Promise((resolve) => { + encryptedClient + .getDecryptedAsInternalUser<SyntheticsMonitorWithSecrets>( + syntheticsMonitor.name, + monitor.id, + { + namespace: monitor.namespaces?.[0], + } + ) + .then((decryptedMonitor) => resolve(decryptedMonitor)) + .catch((e) => { + this.logger.error(e); + sendErrorTelemetryEvents(this.logger, this.server.telemetry, { + reason: 'Failed to decrypt monitor', + message: e?.message, + type: 'runTaskError', + code: e?.code, + status: e.status, + kibanaVersion: this.server.kibanaVersion, + }); + resolve(null); + }); + }) ) ) - ); + ).filter((monitor) => monitor !== null) as Array<SavedObject<SyntheticsMonitorWithSecrets>>; const end = performance.now(); const duration = end - start; @@ -456,14 +473,6 @@ export class SyntheticsService { monitors: monitors.length, }); - if (this.indexTemplateExists) { - // without mapping, querying won't make sense - hydrateSavedObjects({ - monitors: monitors as unknown as DecryptedSyntheticsMonitorSavedObject[], - server: this.server, - }); - } - return (monitors ?? []).map((monitor) => { const attributes = monitor.attributes as unknown as MonitorFields; return formatHeartbeatRequest({ diff --git a/x-pack/plugins/task_manager/README.md b/x-pack/plugins/task_manager/README.md index 4a99889ad9cc31..59f836c10c03eb 100644 --- a/x-pack/plugins/task_manager/README.md +++ b/x-pack/plugins/task_manager/README.md @@ -328,6 +328,9 @@ The _Start_ Plugin api allow you to use Task Manager to facilitate your Plugin's runSoon: (taskId: string) => { // ... }, + bulkEnableDisable: (taskIds: string[], enabled: boolean) => { + // ... + }, bulkUpdateSchedules: (taskIds: string[], schedule: IntervalSchedule) => { // ... }, @@ -418,6 +421,33 @@ export class Plugin { } ``` +#### bulkEnableDisable +Using `bulkEnableDisable` you can instruct TaskManger to update the `enabled` status of tasks. + +Example: +```js +export class Plugin { + constructor() { + } + + public setup(core: CoreSetup, plugins: { taskManager }) { + } + + public start(core: CoreStart, plugins: { taskManager }) { + try { + const bulkDisableResults = await taskManager.bulkEnableDisable( + ['97c2c4e7-d850-11ec-bf95-895ffd19f959', 'a5ee24d1-dce2-11ec-ab8d-cf74da82133d'], + false, + ); + // If no error is thrown, the bulkEnableDisable has completed successfully. + // But some updates of some tasks can be failed, due to OCC 409 conflict for example + } catch(err: Error) { + // if error is caught, means the whole method requested has failed and tasks weren't updated + } + } +} +``` + #### bulkUpdateSchedules Using `bulkUpdatesSchedules` you can instruct TaskManger to update interval of tasks that are in `idle` status (for the tasks which have `running` status, `schedule` and `runAt` will be recalculated after task run finishes). diff --git a/x-pack/plugins/task_manager/server/index.ts b/x-pack/plugins/task_manager/server/index.ts index 0b01d6a05b7e1b..4f1d41cad2109d 100644 --- a/x-pack/plugins/task_manager/server/index.ts +++ b/x-pack/plugins/task_manager/server/index.ts @@ -30,7 +30,7 @@ export { throwUnrecoverableError, isEphemeralTaskRejectedDueToCapacityError, } from './task_running'; -export type { RunNowResult, BulkUpdateSchedulesResult } from './task_scheduling'; +export type { RunNowResult, BulkUpdateTaskResult } from './task_scheduling'; export { getOldestIdleActionTask } from './queries/oldest_idle_action_task'; export { IdleTaskWithExpiredRunAt, diff --git a/x-pack/plugins/task_manager/server/mocks.ts b/x-pack/plugins/task_manager/server/mocks.ts index 1560c20be5baa5..9ce4797e50db96 100644 --- a/x-pack/plugins/task_manager/server/mocks.ts +++ b/x-pack/plugins/task_manager/server/mocks.ts @@ -30,6 +30,7 @@ const createStartMock = () => { supportsEphemeralTasks: jest.fn(), bulkUpdateSchedules: jest.fn(), bulkSchedule: jest.fn(), + bulkEnableDisable: jest.fn(), }; return mock; }; diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 2bb06b3a223be3..08b7a2908f2e8a 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -53,6 +53,7 @@ export type TaskManagerStartContract = Pick< | 'ephemeralRunNow' | 'ensureScheduled' | 'bulkUpdateSchedules' + | 'bulkEnableDisable' | 'bulkSchedule' > & Pick<TaskStore, 'fetch' | 'aggregate' | 'get' | 'remove'> & { @@ -251,6 +252,7 @@ export class TaskManagerPlugin bulkSchedule: (...args) => taskScheduling.bulkSchedule(...args), ensureScheduled: (...args) => taskScheduling.ensureScheduled(...args), runSoon: (...args) => taskScheduling.runSoon(...args), + bulkEnableDisable: (...args) => taskScheduling.bulkEnableDisable(...args), bulkUpdateSchedules: (...args) => taskScheduling.bulkUpdateSchedules(...args), ephemeralRunNow: (task: EphemeralTask) => taskScheduling.ephemeralRunNow(task), supportsEphemeralTasks: () => diff --git a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts index 086a435a39fa9e..4b04672615b7d2 100644 --- a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts +++ b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts @@ -13,6 +13,7 @@ import { IdleTaskWithExpiredRunAt, RunningOrClaimingTaskWithExpiredRetryAt, SortByRunAtAndRetryAt, + EnabledTask, } from './mark_available_tasks_as_claimed'; import { TaskTypeDictionary } from '../task_type_dictionary'; @@ -53,6 +54,8 @@ describe('mark_available_tasks_as_claimed', () => { expect({ query: mustBeAllOf( + // Task must be enabled + EnabledTask, // Either a task with idle status and runAt <= now or // status running or claiming with a retryAt <= now. shouldBeOneOf(IdleTaskWithExpiredRunAt, RunningOrClaimingTaskWithExpiredRetryAt) @@ -72,6 +75,17 @@ describe('mark_available_tasks_as_claimed', () => { query: { bool: { must: [ + { + bool: { + must: [ + { + term: { + 'task.enabled': true, + }, + }, + ], + }, + }, // Either a task with idle status and runAt <= now or // status running or claiming with a retryAt <= now. { diff --git a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts index afdd9c5c18b33f..d477c9c643a492 100644 --- a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts +++ b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts @@ -72,6 +72,18 @@ export const InactiveTasks: MustNotCondition = { }, }; +export const EnabledTask: MustCondition = { + bool: { + must: [ + { + term: { + 'task.enabled': true, + }, + }, + ], + }, +}; + export const RunningOrClaimingTaskWithExpiredRetryAt: MustCondition = { bool: { must: [ diff --git a/x-pack/plugins/task_manager/server/queries/task_claiming.test.ts b/x-pack/plugins/task_manager/server/queries/task_claiming.test.ts index 67175c86370d73..a23c29a5044f30 100644 --- a/x-pack/plugins/task_manager/server/queries/task_claiming.test.ts +++ b/x-pack/plugins/task_manager/server/queries/task_claiming.test.ts @@ -312,6 +312,17 @@ describe('TaskClaiming', () => { expect(query).toMatchObject({ bool: { must: [ + { + bool: { + must: [ + { + term: { + 'task.enabled': true, + }, + }, + ], + }, + }, { bool: { should: [ @@ -437,6 +448,17 @@ if (doc['task.runAt'].size()!=0) { organic: { bool: { must: [ + { + bool: { + must: [ + { + term: { + 'task.enabled': true, + }, + }, + ], + }, + }, { bool: { should: [ @@ -929,6 +951,17 @@ if (doc['task.runAt'].size()!=0) { expect(query).toMatchObject({ bool: { must: [ + { + bool: { + must: [ + { + term: { + 'task.enabled': true, + }, + }, + ], + }, + }, { bool: { should: [ diff --git a/x-pack/plugins/task_manager/server/queries/task_claiming.ts b/x-pack/plugins/task_manager/server/queries/task_claiming.ts index f15639661e5d0c..7226a558549881 100644 --- a/x-pack/plugins/task_manager/server/queries/task_claiming.ts +++ b/x-pack/plugins/task_manager/server/queries/task_claiming.ts @@ -43,6 +43,7 @@ import { SortByRunAtAndRetryAt, tasksClaimedByOwner, tasksOfType, + EnabledTask, } from './mark_available_tasks_as_claimed'; import { TaskTypeDictionary } from '../task_type_dictionary'; import { @@ -384,6 +385,8 @@ export class TaskClaiming { : 'taskTypesToSkip' ); const queryForScheduledTasks = mustBeAllOf( + // Task must be enabled + EnabledTask, // Either a task with idle status and runAt <= now or // status running or claiming with a retryAt <= now. shouldBeOneOf(IdleTaskWithExpiredRunAt, RunningOrClaimingTaskWithExpiredRetryAt) diff --git a/x-pack/plugins/task_manager/server/saved_objects/mappings.json b/x-pack/plugins/task_manager/server/saved_objects/mappings.json index d046a9266cce52..00129ac1bcdd4f 100644 --- a/x-pack/plugins/task_manager/server/saved_objects/mappings.json +++ b/x-pack/plugins/task_manager/server/saved_objects/mappings.json @@ -16,6 +16,9 @@ "retryAt": { "type": "date" }, + "enabled": { + "type": "boolean" + }, "schedule": { "properties": { "interval": { diff --git a/x-pack/plugins/task_manager/server/saved_objects/migrations.test.ts b/x-pack/plugins/task_manager/server/saved_objects/migrations.test.ts index a59fb077dbdeb1..fe8cc3f81eced3 100644 --- a/x-pack/plugins/task_manager/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/task_manager/server/saved_objects/migrations.test.ts @@ -226,6 +226,47 @@ describe('successful migrations', () => { expect(migration820(taskInstance, migrationContext)).toEqual(taskInstance); }); }); + + describe('8.5.0', () => { + test('adds enabled: true to tasks that are running, claiming, or idle', () => { + const migration850 = getMigrations()['8.5.0']; + const activeTasks = [ + getMockData({ + status: 'running', + }), + getMockData({ + status: 'claiming', + }), + getMockData({ + status: 'idle', + }), + ]; + activeTasks.forEach((task) => { + expect(migration850(task, migrationContext)).toEqual({ + ...task, + attributes: { + ...task.attributes, + enabled: true, + }, + }); + }); + }); + + test('does not modify tasks that are failed or unrecognized', () => { + const migration850 = getMigrations()['8.5.0']; + const inactiveTasks = [ + getMockData({ + status: 'failed', + }), + getMockData({ + status: 'unrecognized', + }), + ]; + inactiveTasks.forEach((task) => { + expect(migration850(task, migrationContext)).toEqual(task); + }); + }); + }); }); describe('handles errors during migrations', () => { diff --git a/x-pack/plugins/task_manager/server/saved_objects/migrations.ts b/x-pack/plugins/task_manager/server/saved_objects/migrations.ts index eb4ff6c1b0ecfc..a147a6bdabc6bc 100644 --- a/x-pack/plugins/task_manager/server/saved_objects/migrations.ts +++ b/x-pack/plugins/task_manager/server/saved_objects/migrations.ts @@ -42,6 +42,7 @@ export function getMigrations(): SavedObjectMigrationMap { pipeMigrations(resetAttemptsAndStatusForTheTasksWithoutSchedule, resetUnrecognizedStatus), '8.2.0' ), + '8.5.0': executeMigrationWithErrorHandling(pipeMigrations(addEnabledField), '8.5.0'), }; } @@ -193,3 +194,20 @@ function resetAttemptsAndStatusForTheTasksWithoutSchedule( return doc; } + +function addEnabledField(doc: SavedObjectUnsanitizedDoc<ConcreteTaskInstance>) { + if ( + doc.attributes.status === TaskStatus.Failed || + doc.attributes.status === TaskStatus.Unrecognized + ) { + return doc; + } + + return { + ...doc, + attributes: { + ...doc.attributes, + enabled: true, + }, + }; +} diff --git a/x-pack/plugins/task_manager/server/task.ts b/x-pack/plugins/task_manager/server/task.ts index 6d12a3f5984ca3..ebb957e54699a8 100644 --- a/x-pack/plugins/task_manager/server/task.ts +++ b/x-pack/plugins/task_manager/server/task.ts @@ -277,6 +277,11 @@ export interface TaskInstance { * The random uuid of the Kibana instance which claimed ownership of the task last */ ownerId?: string | null; + + /** + * Indicates whether the task is currently enabled. Disabled tasks will not be claimed. + */ + enabled?: boolean; } /** @@ -371,7 +376,10 @@ export interface ConcreteTaskInstance extends TaskInstance { /** * A task instance that has an id and is ready for storage. */ -export type EphemeralTask = Pick<ConcreteTaskInstance, 'taskType' | 'params' | 'state' | 'scope'>; +export type EphemeralTask = Pick< + ConcreteTaskInstance, + 'taskType' | 'params' | 'state' | 'scope' | 'enabled' +>; export type EphemeralTaskInstance = EphemeralTask & Pick<ConcreteTaskInstance, 'id' | 'scheduledAt' | 'startedAt' | 'runAt' | 'status' | 'ownerId'>; diff --git a/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts b/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts index f61bd762de4588..a9c58b1302f56a 100644 --- a/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts +++ b/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts @@ -179,6 +179,7 @@ describe('TaskManagerRunner', () => { schedule: { interval: `${intervalMinutes}m`, }, + enabled: true, }, definitions: { bar: { @@ -198,6 +199,7 @@ describe('TaskManagerRunner', () => { expect(instance.retryAt!.getTime()).toEqual( instance.startedAt!.getTime() + intervalMinutes * 60 * 1000 ); + expect(instance.enabled).not.toBeDefined(); }); test('calculates retryAt by default timout when it exceeds the schedule of a recurring task', async () => { @@ -211,6 +213,7 @@ describe('TaskManagerRunner', () => { schedule: { interval: `${intervalSeconds}s`, }, + enabled: true, }, definitions: { bar: { @@ -228,6 +231,7 @@ describe('TaskManagerRunner', () => { const instance = store.update.mock.calls[0][0]; expect(instance.retryAt!.getTime()).toEqual(instance.startedAt!.getTime() + 5 * 60 * 1000); + expect(instance.enabled).not.toBeDefined(); }); test('calculates retryAt by timeout if it exceeds the schedule when running a recurring task', async () => { @@ -242,6 +246,7 @@ describe('TaskManagerRunner', () => { schedule: { interval: `${intervalSeconds}s`, }, + enabled: true, }, definitions: { bar: { @@ -262,6 +267,7 @@ describe('TaskManagerRunner', () => { expect(instance.retryAt!.getTime()).toEqual( instance.startedAt!.getTime() + timeoutMinutes * 60 * 1000 ); + expect(instance.enabled).not.toBeDefined(); }); test('sets startedAt, status, attempts and retryAt when claiming a task', async () => { @@ -271,6 +277,7 @@ describe('TaskManagerRunner', () => { const { runner, store } = await pendingStageSetup({ instance: { id, + enabled: true, attempts: initialAttempts, schedule: undefined, }, @@ -296,6 +303,7 @@ describe('TaskManagerRunner', () => { expect(instance.retryAt!.getTime()).toEqual( minutesFromNow((initialAttempts + 1) * 5).getTime() + timeoutMinutes * 60 * 1000 ); + expect(instance.enabled).not.toBeDefined(); }); test('uses getRetry (returning date) to set retryAt when defined', async () => { @@ -309,6 +317,7 @@ describe('TaskManagerRunner', () => { id, attempts: initialAttempts, schedule: undefined, + enabled: true, }, definitions: { bar: { @@ -331,6 +340,7 @@ describe('TaskManagerRunner', () => { expect(instance.retryAt!.getTime()).toEqual( new Date(nextRetry.getTime() + timeoutMinutes * 60 * 1000).getTime() ); + expect(instance.enabled).not.toBeDefined(); }); test('it returns false when markTaskAsRunning fails due to VERSION_CONFLICT_STATUS', async () => { @@ -539,6 +549,7 @@ describe('TaskManagerRunner', () => { id, attempts: initialAttempts, schedule: undefined, + enabled: true, }, definitions: { bar: { @@ -563,6 +574,7 @@ describe('TaskManagerRunner', () => { expect(instance.retryAt!.getTime()).toEqual( new Date(Date.now() + attemptDelay + timeoutDelay).getTime() ); + expect(instance.enabled).not.toBeDefined(); }); test('uses getRetry (returning false) to set retryAt when defined', async () => { @@ -575,6 +587,7 @@ describe('TaskManagerRunner', () => { id, attempts: initialAttempts, schedule: undefined, + enabled: true, }, definitions: { bar: { @@ -596,6 +609,7 @@ describe('TaskManagerRunner', () => { expect(instance.retryAt!).toBeNull(); expect(instance.status).toBe('running'); + expect(instance.enabled).not.toBeDefined(); }); test('bypasses getRetry (returning false) of a recurring task to set retryAt when defined', async () => { @@ -609,6 +623,7 @@ describe('TaskManagerRunner', () => { attempts: initialAttempts, schedule: { interval: '1m' }, startedAt: new Date(), + enabled: true, }, definitions: { bar: { @@ -630,6 +645,7 @@ describe('TaskManagerRunner', () => { const timeoutDelay = timeoutMinutes * 60 * 1000; expect(instance.retryAt!.getTime()).toEqual(new Date(Date.now() + timeoutDelay).getTime()); + expect(instance.enabled).not.toBeDefined(); }); describe('TaskEvents', () => { @@ -781,6 +797,7 @@ describe('TaskManagerRunner', () => { attempts: initialAttempts, params: { a: 'b' }, state: { hey: 'there' }, + enabled: true, }, definitions: { bar: { @@ -803,6 +820,7 @@ describe('TaskManagerRunner', () => { expect(instance.runAt.getTime()).toEqual(minutesFromNow(initialAttempts * 5).getTime()); expect(instance.params).toEqual({ a: 'b' }); expect(instance.state).toEqual({ hey: 'there' }); + expect(instance.enabled).not.toBeDefined(); }); test('reschedules tasks that have an schedule', async () => { @@ -811,6 +829,7 @@ describe('TaskManagerRunner', () => { schedule: { interval: '10m' }, status: TaskStatus.Running, startedAt: new Date(), + enabled: true, }, definitions: { bar: { @@ -831,6 +850,7 @@ describe('TaskManagerRunner', () => { expect(instance.runAt.getTime()).toBeGreaterThan(minutesFromNow(9).getTime()); expect(instance.runAt.getTime()).toBeLessThanOrEqual(minutesFromNow(10).getTime()); + expect(instance.enabled).not.toBeDefined(); }); test('expiration returns time after which timeout will have elapsed from start', async () => { @@ -951,6 +971,7 @@ describe('TaskManagerRunner', () => { schedule: { interval: '20m' }, status: TaskStatus.Running, startedAt: new Date(), + enabled: true, }, definitions: { bar: { @@ -968,6 +989,7 @@ describe('TaskManagerRunner', () => { const instance = store.update.mock.calls[0][0]; expect(instance.status).toBe('failed'); + expect(instance.enabled).not.toBeDefined(); expect(onTaskEvent).toHaveBeenCalledWith( withAnyTiming( @@ -1092,6 +1114,7 @@ describe('TaskManagerRunner', () => { instance: { id, attempts: initialAttempts, + enabled: true, }, definitions: { bar: { @@ -1113,6 +1136,7 @@ describe('TaskManagerRunner', () => { const instance = store.update.mock.calls[0][0]; expect(instance.runAt.getTime()).toEqual(nextRetry.getTime()); + expect(instance.enabled).not.toBeDefined(); }); test('uses getRetry function (returning true) on error when defined', async () => { @@ -1124,6 +1148,7 @@ describe('TaskManagerRunner', () => { instance: { id, attempts: initialAttempts, + enabled: true, }, definitions: { bar: { @@ -1146,6 +1171,7 @@ describe('TaskManagerRunner', () => { const expectedRunAt = new Date(Date.now() + initialAttempts * 5 * 60 * 1000); expect(instance.runAt.getTime()).toEqual(expectedRunAt.getTime()); + expect(instance.enabled).not.toBeDefined(); }); test('uses getRetry function (returning false) on error when defined', async () => { @@ -1157,6 +1183,7 @@ describe('TaskManagerRunner', () => { instance: { id, attempts: initialAttempts, + enabled: true, }, definitions: { bar: { @@ -1178,6 +1205,7 @@ describe('TaskManagerRunner', () => { const instance = store.update.mock.calls[0][0]; expect(instance.status).toBe('failed'); + expect(instance.enabled).not.toBeDefined(); }); test('bypasses getRetry function (returning false) on error of a recurring task', async () => { @@ -1191,6 +1219,7 @@ describe('TaskManagerRunner', () => { attempts: initialAttempts, schedule: { interval: '1m' }, startedAt: new Date(), + enabled: true, }, definitions: { bar: { @@ -1214,6 +1243,7 @@ describe('TaskManagerRunner', () => { const nextIntervalDelay = 60000; // 1m const expectedRunAt = new Date(Date.now() + nextIntervalDelay); expect(instance.runAt.getTime()).toEqual(expectedRunAt.getTime()); + expect(instance.enabled).not.toBeDefined(); }); test('Fails non-recurring task when maxAttempts reached', async () => { @@ -1224,6 +1254,7 @@ describe('TaskManagerRunner', () => { id, attempts: initialAttempts, schedule: undefined, + enabled: true, }, definitions: { bar: { @@ -1246,6 +1277,7 @@ describe('TaskManagerRunner', () => { expect(instance.status).toEqual('failed'); expect(instance.retryAt!).toBeNull(); expect(instance.runAt.getTime()).toBeLessThanOrEqual(Date.now()); + expect(instance.enabled).not.toBeDefined(); }); test(`Doesn't fail recurring tasks when maxAttempts reached`, async () => { @@ -1258,6 +1290,7 @@ describe('TaskManagerRunner', () => { attempts: initialAttempts, schedule: { interval: `${intervalSeconds}s` }, startedAt: new Date(), + enabled: true, }, definitions: { bar: { @@ -1281,6 +1314,7 @@ describe('TaskManagerRunner', () => { expect(instance.runAt.getTime()).toEqual( new Date(Date.now() + intervalSeconds * 1000).getTime() ); + expect(instance.enabled).not.toBeDefined(); }); describe('TaskEvents', () => { @@ -1450,6 +1484,7 @@ describe('TaskManagerRunner', () => { instance: { id, startedAt: new Date(), + enabled: true, }, definitions: { bar: { @@ -1468,6 +1503,7 @@ describe('TaskManagerRunner', () => { const instance = store.update.mock.calls[0][0]; expect(instance.status).toBe('failed'); + expect(instance.enabled).not.toBeDefined(); expect(onTaskEvent).toHaveBeenCalledWith( withAnyTiming( diff --git a/x-pack/plugins/task_manager/server/task_running/task_runner.ts b/x-pack/plugins/task_manager/server/task_running/task_runner.ts index 0b735d6b0ede60..a5865abc46bbe0 100644 --- a/x-pack/plugins/task_manager/server/task_running/task_runner.ts +++ b/x-pack/plugins/task_manager/server/task_running/task_runner.ts @@ -14,7 +14,7 @@ import apm from 'elastic-apm-node'; import uuid from 'uuid'; import { withSpan } from '@kbn/apm-utils'; -import { identity, defaults, flow } from 'lodash'; +import { identity, defaults, flow, omit } from 'lodash'; import { Logger, SavedObjectsErrorHelpers, ExecutionContextStart } from '@kbn/core/server'; import { UsageCounter } from '@kbn/usage-collection-plugin/server'; import { Middleware } from '../lib/middleware'; @@ -375,7 +375,7 @@ export class TaskManagerRunner implements TaskRunner { this.instance = asReadyToRun( (await this.bufferedTaskStore.update({ - ...taskInstance, + ...taskWithoutEnabled(taskInstance), status: TaskStatus.Running, startedAt: now, attempts, @@ -456,7 +456,7 @@ export class TaskManagerRunner implements TaskRunner { private async releaseClaimAndIncrementAttempts(): Promise<Result<ConcreteTaskInstance, Error>> { return promiseResult( this.bufferedTaskStore.update({ - ...this.instance.task, + ...taskWithoutEnabled(this.instance.task), status: TaskStatus.Idle, attempts: this.instance.task.attempts + 1, startedAt: null, @@ -549,7 +549,7 @@ export class TaskManagerRunner implements TaskRunner { retryAt: null, ownerId: null, }, - this.instance.task + taskWithoutEnabled(this.instance.task) ) ) ); @@ -677,6 +677,12 @@ function howManyMsUntilOwnershipClaimExpires(ownershipClaimedUntil: Date | null) return ownershipClaimedUntil ? ownershipClaimedUntil.getTime() - Date.now() : 0; } +// Omits "enabled" field from task updates so we don't overwrite any user +// initiated changes to "enabled" while the task was running +function taskWithoutEnabled(task: ConcreteTaskInstance): ConcreteTaskInstance { + return omit(task, 'enabled'); +} + // A type that extracts the Instance type out of TaskRunningStage // This helps us to better communicate to the developer what the expected "stage" // in a specific place in the code might be diff --git a/x-pack/plugins/task_manager/server/task_scheduling.mock.ts b/x-pack/plugins/task_manager/server/task_scheduling.mock.ts index 15c8dc06c473a8..08f36661dde52c 100644 --- a/x-pack/plugins/task_manager/server/task_scheduling.mock.ts +++ b/x-pack/plugins/task_manager/server/task_scheduling.mock.ts @@ -9,6 +9,7 @@ import { TaskScheduling } from './task_scheduling'; const createTaskSchedulingMock = () => { return { + bulkEnableDisable: jest.fn(), ensureScheduled: jest.fn(), schedule: jest.fn(), runSoon: jest.fn(), diff --git a/x-pack/plugins/task_manager/server/task_scheduling.test.ts b/x-pack/plugins/task_manager/server/task_scheduling.test.ts index a94394527a61d0..071b0147e19b24 100644 --- a/x-pack/plugins/task_manager/server/task_scheduling.test.ts +++ b/x-pack/plugins/task_manager/server/task_scheduling.test.ts @@ -70,6 +70,26 @@ describe('TaskScheduling', () => { id: undefined, schedule: undefined, traceparent: 'parent', + enabled: true, + }); + }); + + test('allows scheduling tasks that are disabled', async () => { + const taskScheduling = new TaskScheduling(taskSchedulingOpts); + const task = { + taskType: 'foo', + enabled: false, + params: {}, + state: {}, + }; + await taskScheduling.schedule(task); + expect(mockTaskStore.schedule).toHaveBeenCalled(); + expect(mockTaskStore.schedule).toHaveBeenCalledWith({ + ...task, + id: undefined, + schedule: undefined, + traceparent: 'parent', + enabled: false, }); }); @@ -125,6 +145,133 @@ describe('TaskScheduling', () => { }); }); + describe('bulkEnableDisable', () => { + const id = '01ddff11-e88a-4d13-bc4e-256164e755e2'; + beforeEach(() => { + mockTaskStore.bulkUpdate.mockImplementation(() => + Promise.resolve([{ tag: 'ok', value: mockTask() }]) + ); + }); + + test('should search for tasks by ids enabled = true when disabling', async () => { + mockTaskStore.fetch.mockResolvedValue({ docs: [] }); + const taskScheduling = new TaskScheduling(taskSchedulingOpts); + + await taskScheduling.bulkEnableDisable([id], false); + + expect(mockTaskStore.fetch).toHaveBeenCalledTimes(1); + expect(mockTaskStore.fetch).toHaveBeenCalledWith({ + query: { + bool: { + must: [ + { + terms: { + _id: [`task:${id}`], + }, + }, + { + term: { + 'task.enabled': true, + }, + }, + ], + }, + }, + size: 100, + }); + }); + + test('should search for tasks by ids enabled = false when enabling', async () => { + mockTaskStore.fetch.mockResolvedValue({ docs: [] }); + const taskScheduling = new TaskScheduling(taskSchedulingOpts); + + await taskScheduling.bulkEnableDisable([id], true); + + expect(mockTaskStore.fetch).toHaveBeenCalledTimes(1); + expect(mockTaskStore.fetch).toHaveBeenCalledWith({ + query: { + bool: { + must: [ + { + terms: { + _id: [`task:${id}`], + }, + }, + { + term: { + 'task.enabled': false, + }, + }, + ], + }, + }, + size: 100, + }); + }); + + test('should split search on chunks when input ids array too large', async () => { + mockTaskStore.fetch.mockResolvedValue({ docs: [] }); + const taskScheduling = new TaskScheduling(taskSchedulingOpts); + + await taskScheduling.bulkEnableDisable(Array.from({ length: 1250 }), false); + + expect(mockTaskStore.fetch).toHaveBeenCalledTimes(13); + }); + + test('should transform response into correct format', async () => { + const successfulTask = mockTask({ + id: 'task-1', + enabled: false, + schedule: { interval: '1h' }, + }); + const failedTask = mockTask({ id: 'task-2', enabled: true, schedule: { interval: '1h' } }); + mockTaskStore.bulkUpdate.mockImplementation(() => + Promise.resolve([ + { tag: 'ok', value: successfulTask }, + { tag: 'err', error: { entity: failedTask, error: new Error('fail') } }, + ]) + ); + mockTaskStore.fetch.mockResolvedValue({ docs: [successfulTask, failedTask] }); + + const taskScheduling = new TaskScheduling(taskSchedulingOpts); + const result = await taskScheduling.bulkEnableDisable( + [successfulTask.id, failedTask.id], + false + ); + + expect(result).toEqual({ + tasks: [successfulTask], + errors: [{ task: failedTask, error: new Error('fail') }], + }); + }); + + test('should not disable task if it is already disabled', async () => { + const task = mockTask({ id, enabled: false, schedule: { interval: '3h' } }); + + mockTaskStore.fetch.mockResolvedValue({ docs: [task] }); + + const taskScheduling = new TaskScheduling(taskSchedulingOpts); + await taskScheduling.bulkEnableDisable([id], false); + + const bulkUpdatePayload = mockTaskStore.bulkUpdate.mock.calls[0][0]; + + expect(bulkUpdatePayload).toHaveLength(0); + }); + + test('should not enable task if it is already enabled', async () => { + const task = mockTask({ id, enabled: true, schedule: { interval: '3h' } }); + + mockTaskStore.fetch.mockResolvedValue({ docs: [task] }); + + const taskScheduling = new TaskScheduling(taskSchedulingOpts); + await taskScheduling.bulkEnableDisable([id], true); + + const bulkUpdatePayload = mockTaskStore.bulkUpdate.mock.calls[0][0]; + + expect(bulkUpdatePayload).toHaveLength(0); + }); + }); + describe('bulkUpdateSchedules', () => { const id = '01ddff11-e88a-4d13-bc4e-256164e755e2'; beforeEach(() => { @@ -258,6 +405,7 @@ describe('TaskScheduling', () => { expect(bulkUpdatePayload[0].runAt.getTime()).toBeLessThanOrEqual(Date.now()); }); }); + describe('runSoon', () => { test('resolves when the task update succeeds', async () => { const id = '01ddff11-e88a-4d13-bc4e-256164e755e2'; @@ -513,6 +661,40 @@ describe('TaskScheduling', () => { id: undefined, schedule: undefined, traceparent: 'parent', + enabled: true, + }, + ]); + }); + + test('allows scheduling tasks that are disabled', async () => { + const taskScheduling = new TaskScheduling(taskSchedulingOpts); + const task1 = { + taskType: 'foo', + params: {}, + state: {}, + }; + const task2 = { + taskType: 'foo', + params: {}, + state: {}, + enabled: false, + }; + await taskScheduling.bulkSchedule([task1, task2]); + expect(mockTaskStore.bulkSchedule).toHaveBeenCalled(); + expect(mockTaskStore.bulkSchedule).toHaveBeenCalledWith([ + { + ...task1, + id: undefined, + schedule: undefined, + traceparent: 'parent', + enabled: true, + }, + { + ...task2, + id: undefined, + schedule: undefined, + traceparent: 'parent', + enabled: false, }, ]); }); @@ -546,6 +728,7 @@ function mockTask(overrides: Partial<ConcreteTaskInstance> = {}): ConcreteTaskIn taskType: 'foo', schedule: undefined, attempts: 0, + enabled: true, status: TaskStatus.Claiming, params: { hello: 'world' }, state: { baby: 'Henhen' }, diff --git a/x-pack/plugins/task_manager/server/task_scheduling.ts b/x-pack/plugins/task_manager/server/task_scheduling.ts index 9434ba4fba0c31..8cd3330052cf46 100644 --- a/x-pack/plugins/task_manager/server/task_scheduling.ts +++ b/x-pack/plugins/task_manager/server/task_scheduling.ts @@ -48,7 +48,7 @@ import { EphemeralTaskLifecycle } from './ephemeral_task_lifecycle'; import { EphemeralTaskRejectedDueToCapacityError } from './task_running'; const VERSION_CONFLICT_STATUS = 409; - +const BULK_ACTION_SIZE = 100; export interface TaskSchedulingOpts { logger: Logger; taskStore: TaskStore; @@ -61,7 +61,7 @@ export interface TaskSchedulingOpts { /** * return type of TaskScheduling.bulkUpdateSchedules method */ -export interface BulkUpdateSchedulesResult { +export interface BulkUpdateTaskResult { /** * list of successfully updated tasks */ @@ -126,6 +126,7 @@ export class TaskScheduling { return await this.store.schedule({ ...modifiedTask, traceparent: traceparent || '', + enabled: modifiedTask.enabled ?? true, }); } @@ -149,13 +150,72 @@ export class TaskScheduling { ...options, taskInstance: ensureDeprecatedFieldsAreCorrected(taskInstance, this.logger), }); - return { ...modifiedTask, traceparent: traceparent || '' }; + return { + ...modifiedTask, + traceparent: traceparent || '', + enabled: modifiedTask.enabled ?? true, + }; }) ); return await this.store.bulkSchedule(modifiedTasks); } + public async bulkEnableDisable( + taskIds: string[], + enabled: boolean + ): Promise<BulkUpdateTaskResult> { + const tasks = await pMap( + chunk(taskIds, BULK_ACTION_SIZE), + async (taskIdsChunk) => + this.store.fetch({ + query: { + bool: { + must: [ + { + terms: { + _id: taskIdsChunk.map((taskId) => `task:${taskId}`), + }, + }, + { + term: { + 'task.enabled': !enabled, + }, + }, + ], + }, + }, + size: BULK_ACTION_SIZE, + }), + { concurrency: 10 } + ); + + const updatedTasks = tasks + .flatMap(({ docs }) => docs) + .reduce<ConcreteTaskInstance[]>((acc, task) => { + // if task is not enabled, no need to update it + if (enabled === task.enabled) { + return acc; + } + + acc.push({ ...task, enabled }); + return acc; + }, []); + + return (await this.store.bulkUpdate(updatedTasks)).reduce<BulkUpdateTaskResult>( + (acc, task) => { + if (task.tag === 'ok') { + acc.tasks.push(task.value); + } else { + acc.errors.push({ error: task.error.error, task: task.error.entity }); + } + + return acc; + }, + { tasks: [], errors: [] } + ); + } + /** * Bulk updates schedules for tasks by ids. * Only tasks with `idle` status will be updated, as for the tasks which have `running` status, @@ -163,14 +223,14 @@ export class TaskScheduling { * * @param {string[]} taskIds - list of task ids * @param {IntervalSchedule} schedule - new schedule - * @returns {Promise<BulkUpdateSchedulesResult>} + * @returns {Promise<BulkUpdateTaskResult>} */ public async bulkUpdateSchedules( taskIds: string[], schedule: IntervalSchedule - ): Promise<BulkUpdateSchedulesResult> { + ): Promise<BulkUpdateTaskResult> { const tasks = await pMap( - chunk(taskIds, 100), + chunk(taskIds, BULK_ACTION_SIZE), async (taskIdsChunk) => this.store.fetch({ query: mustBeAllOf( @@ -185,7 +245,7 @@ export class TaskScheduling { }, } ), - size: 100, + size: BULK_ACTION_SIZE, }), { concurrency: 10 } ); @@ -211,7 +271,7 @@ export class TaskScheduling { return acc; }, []); - return (await this.store.bulkUpdate(updatedTasks)).reduce<BulkUpdateSchedulesResult>( + return (await this.store.bulkUpdate(updatedTasks)).reduce<BulkUpdateTaskResult>( (acc, task) => { if (task.tag === 'ok') { acc.tasks.push(task.value); @@ -226,7 +286,7 @@ export class TaskScheduling { } /** - * Run task. + * Run task. * * @param taskId - The task being scheduled. * @returns {Promise<RunSoonResult>} diff --git a/x-pack/plugins/threat_intelligence/README.md b/x-pack/plugins/threat_intelligence/README.md index 8c9c690924218a..945ab9b85a4f1f 100755 --- a/x-pack/plugins/threat_intelligence/README.md +++ b/x-pack/plugins/threat_intelligence/README.md @@ -19,7 +19,7 @@ Verify your node version [here](https://github.com/elastic/kibana/blob/main/.nod **Run Kibana:** > **Important:** -> +> > See here to get your `kibana.yaml` to enable the Threat Intelligence plugin. ``` @@ -27,6 +27,16 @@ yarn kbn reset && yarn kbn bootstrap yarn start --no-base-path ``` +### Performance + +You can generate large volumes of threat indicators on demand with the following script: + +``` +node scripts/generate_indicators.js +``` + +see the file in order to adjust the amount of indicators generated. The default is one million. + ### Useful hints Export local instance data to es_archives (will be loaded in cypress tests). @@ -45,4 +55,4 @@ See [CONTRIBUTING.md](https://github.com/elastic/kibana/blob/main/x-pack/plugins ## Issues -Please report any issues in [this GitHub project](https://github.com/orgs/elastic/projects/758/). \ No newline at end of file +Please report any issues in [this GitHub project](https://github.com/orgs/elastic/projects/758/). diff --git a/x-pack/plugins/threat_intelligence/common/types/component_type.ts b/x-pack/plugins/threat_intelligence/common/types/component_type.ts new file mode 100644 index 00000000000000..d5982b15c35189 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/common/types/component_type.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. + */ + +/** + * Used in multiple component to drive the render of the component depending on where they're used. + */ +export enum ComponentType { + EuiDataGrid = 'EuiDataGrid', + ContextMenu = 'ContextMenu', +} diff --git a/x-pack/plugins/threat_intelligence/common/types/indicator.ts b/x-pack/plugins/threat_intelligence/common/types/indicator.ts index 2edcbb5a829ea6..69e76e18545c1e 100644 --- a/x-pack/plugins/threat_intelligence/common/types/indicator.ts +++ b/x-pack/plugins/threat_intelligence/common/types/indicator.ts @@ -10,6 +10,7 @@ */ export enum RawIndicatorFieldId { Type = 'threat.indicator.type', + Confidence = 'threat.indicator.confidence', FirstSeen = 'threat.indicator.first_seen', LastSeen = 'threat.indicator.last_seen', MarkingTLP = 'threat.indicator.marking.tlp', @@ -44,6 +45,7 @@ export enum RawIndicatorFieldId { TimeStamp = '@timestamp', Id = '_id', Name = 'threat.indicator.name', + Description = 'threat.indicator.description', NameOrigin = 'threat.indicator.name_origin', } diff --git a/x-pack/plugins/threat_intelligence/cypress/cypress.config.ts b/x-pack/plugins/threat_intelligence/cypress/cypress.config.ts new file mode 100644 index 00000000000000..540485920c2ffe --- /dev/null +++ b/x-pack/plugins/threat_intelligence/cypress/cypress.config.ts @@ -0,0 +1,37 @@ +/* + * 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. + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import { defineConfig } from 'cypress'; + +// eslint-disable-next-line import/no-default-export +export default defineConfig({ + defaultCommandTimeout: 120000, + execTimeout: 120000, + pageLoadTimeout: 120000, + retries: { + runMode: 2, + }, + screenshotsFolder: '../../../target/kibana-threat-intelligence/cypress/screenshots', + trashAssetsBeforeRuns: false, + video: false, + videosFolder: '../../../target/kibana-threat-intelligence/cypress/videos', + viewportHeight: 946, + viewportWidth: 1680, + env: { + protocol: 'http', + hostname: 'localhost', + configport: '5601', + }, + e2e: { + baseUrl: 'http://localhost:5601', + setupNodeEvents(on, config) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require('./plugins')(on, config); + }, + }, +}); diff --git a/x-pack/plugins/threat_intelligence/cypress/cypress.json b/x-pack/plugins/threat_intelligence/cypress/cypress.json deleted file mode 100644 index 90b6cdb52e3c17..00000000000000 --- a/x-pack/plugins/threat_intelligence/cypress/cypress.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "baseUrl": "http://localhost:5601", - "defaultCommandTimeout": 120000, - "execTimeout": 120000, - "pageLoadTimeout": 120000, - "retries": { - "runMode": 2 - }, - "screenshotsFolder": "../../../target/kibana-threat-intelligence/cypress/screenshots", - "trashAssetsBeforeRuns": false, - "video": false, - "videosFolder": "../../../target/kibana-threat-intelligence/cypress/videos", - "viewportHeight": 946, - "viewportWidth": 1680, - "env": { - "protocol": "http", - "hostname": "localhost", - "configport": "5601" - } -} diff --git a/x-pack/plugins/threat_intelligence/cypress/integration/empty_page.spec.ts b/x-pack/plugins/threat_intelligence/cypress/e2e/empty_page.cy.ts similarity index 100% rename from x-pack/plugins/threat_intelligence/cypress/integration/empty_page.spec.ts rename to x-pack/plugins/threat_intelligence/cypress/e2e/empty_page.cy.ts diff --git a/x-pack/plugins/threat_intelligence/cypress/integration/indicators.spec.ts b/x-pack/plugins/threat_intelligence/cypress/e2e/indicators.cy.ts similarity index 95% rename from x-pack/plugins/threat_intelligence/cypress/integration/indicators.spec.ts rename to x-pack/plugins/threat_intelligence/cypress/e2e/indicators.cy.ts index d331f2937e41d6..ccd91253012b39 100644 --- a/x-pack/plugins/threat_intelligence/cypress/integration/indicators.spec.ts +++ b/x-pack/plugins/threat_intelligence/cypress/e2e/indicators.cy.ts @@ -78,12 +78,16 @@ describe('Indicators', () => { cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true }); - cy.get(FLYOUT_TITLE).should('contain', 'Indicator:'); + cy.get(FLYOUT_TITLE).should('contain', 'Indicator details'); - cy.get(FLYOUT_TABLE).should('exist').and('contain.text', 'threat.indicator.type'); + cy.get(FLYOUT_TABS).should('exist').children().should('have.length', 3); + + cy.get(FLYOUT_TABS).should('exist'); + cy.get(`${FLYOUT_TABS} button:nth-child(2)`).click(); - cy.get(FLYOUT_TABS).should('exist').children().should('have.length', 2).last().click(); + cy.get(FLYOUT_TABLE).should('exist').and('contain.text', 'threat.indicator.type'); + cy.get(`${FLYOUT_TABS} button:nth-child(3)`).click(); cy.get(FLYOUT_JSON).should('exist').and('contain.text', 'threat.indicator.type'); }); }); diff --git a/x-pack/plugins/threat_intelligence/cypress/e2e/query_bar.cy.ts b/x-pack/plugins/threat_intelligence/cypress/e2e/query_bar.cy.ts new file mode 100644 index 00000000000000..e2a0459dc3fd6a --- /dev/null +++ b/x-pack/plugins/threat_intelligence/cypress/e2e/query_bar.cy.ts @@ -0,0 +1,148 @@ +/* + * 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 { + INDICATOR_TYPE_CELL, + TOGGLE_FLYOUT_BUTTON, + FLYOUT_CLOSE_BUTTON, + KQL_FILTER, + INDICATORS_TABLE_CELL_FILTER_IN_BUTTON, + INDICATORS_TABLE_CELL_FILTER_OUT_BUTTON, + FLYOUT_TABLE_TAB_ROW_FILTER_IN_BUTTON, + FLYOUT_TABLE_TAB_ROW_FILTER_OUT_BUTTON, + BARCHART_POPOVER_BUTTON, + BARCHART_FILTER_IN_BUTTON, + BARCHART_FILTER_OUT_BUTTON, + FLYOUT_OVERVIEW_TAB_BLOCKS_FILTER_IN_BUTTON, + FLYOUT_OVERVIEW_TAB_BLOCKS_FILTER_OUT_BUTTON, + FLYOUT_OVERVIEW_TAB_TABLE_ROW_FILTER_IN_BUTTON, + FLYOUT_OVERVIEW_TAB_TABLE_ROW_FILTER_OUT_BUTTON, + FLYOUT_OVERVIEW_TAB_BLOCKS_ITEM, + FLYOUT_TABS, +} from '../screens/indicators'; +import { selectRange } from '../tasks/select_range'; +import { login } from '../tasks/login'; +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; + +before(() => { + login(); +}); + +const THREAT_INTELLIGENCE = '/app/security/threat_intelligence/indicators'; + +describe('Indicators', () => { + before(() => { + esArchiverLoad('threat_intelligence'); + }); + after(() => { + esArchiverUnload('threat_intelligence'); + }); + + describe('Indicators query bar interaction', () => { + before(() => { + cy.visit(THREAT_INTELLIGENCE); + + selectRange(); + }); + + it('should add filter to kql and filter in values when clicking in the barchart legend', () => { + cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0); + cy.get(BARCHART_POPOVER_BUTTON).should('exist').first().click(); + cy.get(BARCHART_FILTER_IN_BUTTON).should('exist').click(); + cy.get(KQL_FILTER).should('exist'); + cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0); + }); + + it('should add negated filter to kql and filter out values when clicking in the barchart legend', () => { + cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0); + cy.get(BARCHART_POPOVER_BUTTON).should('exist').first().click(); + cy.get(BARCHART_FILTER_OUT_BUTTON).should('exist').click(); + cy.get(KQL_FILTER).should('exist'); + cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0); + }); + + it('should add filter to kql and filter in and out values when clicking in an indicators table cell', () => { + cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0); + cy.get(INDICATOR_TYPE_CELL).first().trigger('mouseover'); + cy.get(INDICATORS_TABLE_CELL_FILTER_IN_BUTTON).should('exist').click(); + cy.get(KQL_FILTER).should('exist'); + cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0); + }); + + it('should add negated filter and filter out and out values when clicking in an indicators table cell', () => { + cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0); + cy.get(INDICATOR_TYPE_CELL).first().trigger('mouseover'); + cy.get(INDICATORS_TABLE_CELL_FILTER_OUT_BUTTON).should('exist').click(); + cy.get(KQL_FILTER).should('exist'); + cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0); + }); + + it('should add filter to kql and filter in values when clicking in an indicators flyout overview tab block', () => { + cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0); + cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true }); + cy.get(FLYOUT_OVERVIEW_TAB_BLOCKS_ITEM).first().trigger('mouseover'); + cy.get(FLYOUT_OVERVIEW_TAB_BLOCKS_FILTER_IN_BUTTON) + .should('exist') + .first() + .click({ force: true }); + cy.get(FLYOUT_CLOSE_BUTTON).should('exist').click(); + cy.get(KQL_FILTER).should('exist'); + cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0); + }); + + it('should add negated filter to kql filter out values when clicking in an indicators flyout overview block', () => { + cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0); + cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true }); + cy.get(FLYOUT_OVERVIEW_TAB_BLOCKS_ITEM).first().trigger('mouseover'); + cy.get(FLYOUT_OVERVIEW_TAB_BLOCKS_FILTER_OUT_BUTTON) + .should('exist') + .first() + .click({ force: true }); + cy.get(FLYOUT_CLOSE_BUTTON).should('exist').click(); + cy.get(KQL_FILTER).should('exist'); + cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0); + }); + + it('should add filter to kql and filter in values when clicking in an indicators flyout overview tab table row', () => { + cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0); + cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true }); + cy.get(FLYOUT_OVERVIEW_TAB_TABLE_ROW_FILTER_IN_BUTTON).should('exist').first().click(); + cy.get(FLYOUT_CLOSE_BUTTON).should('exist').click(); + cy.get(KQL_FILTER).should('exist'); + cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0); + }); + + it('should add negated filter to kql filter out values when clicking in an indicators flyout overview tab row', () => { + cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0); + cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true }); + cy.get(FLYOUT_OVERVIEW_TAB_TABLE_ROW_FILTER_OUT_BUTTON).should('exist').first().click(); + cy.get(FLYOUT_CLOSE_BUTTON).should('exist').click(); + cy.get(KQL_FILTER).should('exist'); + cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0); + }); + + it('should add filter to kql and filter in values when clicking in an indicators flyout table tab action column', () => { + cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0); + cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true }); + cy.get(`${FLYOUT_TABS} button:nth-child(2)`).click(); + cy.get(FLYOUT_TABLE_TAB_ROW_FILTER_IN_BUTTON).should('exist').first().click(); + cy.get(FLYOUT_CLOSE_BUTTON).should('exist').click(); + cy.get(KQL_FILTER).should('exist'); + cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0); + }); + + it('should add negated filter to kql filter out values when clicking in an indicators flyout table tab action column', () => { + cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0); + cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true }); + cy.get(`${FLYOUT_TABS} button:nth-child(2)`).click(); + cy.get(FLYOUT_TABLE_TAB_ROW_FILTER_OUT_BUTTON).should('exist').first().click(); + cy.get(FLYOUT_CLOSE_BUTTON).should('exist').click(); + cy.get(KQL_FILTER).should('exist'); + cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0); + }); + }); +}); diff --git a/x-pack/plugins/threat_intelligence/cypress/integration/timeline.spec.ts b/x-pack/plugins/threat_intelligence/cypress/e2e/timeline.cy.ts similarity index 57% rename from x-pack/plugins/threat_intelligence/cypress/integration/timeline.spec.ts rename to x-pack/plugins/threat_intelligence/cypress/e2e/timeline.cy.ts index 8235a9ac513ae8..9077ef0fe49808 100644 --- a/x-pack/plugins/threat_intelligence/cypress/integration/timeline.spec.ts +++ b/x-pack/plugins/threat_intelligence/cypress/e2e/timeline.cy.ts @@ -6,14 +6,19 @@ */ import { + BARCHART_POPOVER_BUTTON, BARCHART_TIMELINE_BUTTON, FLYOUT_CLOSE_BUTTON, - FLYOUT_TABLE_ROW_TIMELINE_BUTTON, + FLYOUT_OVERVIEW_TAB_TABLE_ROW_TIMELINE_BUTTON, + FLYOUT_TABLE_TAB_ROW_TIMELINE_BUTTON, + FLYOUT_TABS, INDICATOR_TYPE_CELL, INDICATORS_TABLE_CELL_TIMELINE_BUTTON, TIMELINE_DRAGGABLE_ITEM, TOGGLE_FLYOUT_BUTTON, UNTITLED_TIMELINE_BUTTON, + FLYOUT_OVERVIEW_TAB_BLOCKS_TIMELINE_BUTTON, + FLYOUT_OVERVIEW_TAB_BLOCKS_ITEM, } from '../screens/indicators'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { login } from '../tasks/login'; @@ -41,6 +46,7 @@ describe('Indicators', () => { }); it('should add entry in timeline when clicking in the barchart legend', () => { + cy.get(BARCHART_POPOVER_BUTTON).should('exist').first().click(); cy.get(BARCHART_TIMELINE_BUTTON).should('exist').first().click(); cy.get(UNTITLED_TIMELINE_BUTTON).should('exist').first().click(); cy.get(TIMELINE_DRAGGABLE_ITEM).should('exist'); @@ -53,9 +59,31 @@ describe('Indicators', () => { cy.get(TIMELINE_DRAGGABLE_ITEM).should('exist'); }); - it('should add entry in timeline when clicking in an indicators flyout row', () => { + it('should add entry in timeline when clicking in an indicator flyout overview tab table row', () => { cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true }); - cy.get(FLYOUT_TABLE_ROW_TIMELINE_BUTTON).should('exist').first().click(); + cy.get(FLYOUT_OVERVIEW_TAB_TABLE_ROW_TIMELINE_BUTTON).should('exist').first().click(); + cy.get(FLYOUT_CLOSE_BUTTON).should('exist').click(); + cy.get(UNTITLED_TIMELINE_BUTTON).should('exist').first().click(); + cy.get(TIMELINE_DRAGGABLE_ITEM).should('exist'); + }); + + it('should add entry in timeline when clicking in an indicator flyout overview block', () => { + cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true }); + cy.get(FLYOUT_OVERVIEW_TAB_BLOCKS_ITEM).first().trigger('mouseover'); + cy.get(FLYOUT_OVERVIEW_TAB_BLOCKS_TIMELINE_BUTTON) + .should('exist') + .first() + .click({ force: true }); + cy.get(FLYOUT_CLOSE_BUTTON).should('exist').click(); + cy.get(UNTITLED_TIMELINE_BUTTON).should('exist').first().click(); + cy.get(TIMELINE_DRAGGABLE_ITEM).should('exist'); + }); + + it('should add entry in timeline when clicking in an indicator flyout table tab', () => { + cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true }); + cy.get(FLYOUT_TABS).should('exist'); + cy.get(`${FLYOUT_TABS} button:nth-child(2)`).click(); + cy.get(FLYOUT_TABLE_TAB_ROW_TIMELINE_BUTTON).should('exist').first().click(); cy.get(FLYOUT_CLOSE_BUTTON).should('exist').click(); cy.get(UNTITLED_TIMELINE_BUTTON).should('exist').first().click(); cy.get(TIMELINE_DRAGGABLE_ITEM).should('exist'); diff --git a/x-pack/plugins/threat_intelligence/cypress/integration/query_bar.spec.ts b/x-pack/plugins/threat_intelligence/cypress/integration/query_bar.spec.ts deleted file mode 100644 index 39d1c0aec2af8a..00000000000000 --- a/x-pack/plugins/threat_intelligence/cypress/integration/query_bar.spec.ts +++ /dev/null @@ -1,63 +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 { - FILTER_IN_BUTTON, - FILTER_OUT_BUTTON, - FILTER_IN_COMPONENT, - FILTER_OUT_COMPONENT, - INDICATOR_TYPE_CELL, - TOGGLE_FLYOUT_BUTTON, - FLYOUT_CLOSE_BUTTON, - KQL_FILTER, -} from '../screens/indicators'; -import { selectRange } from '../tasks/select_range'; -import { login } from '../tasks/login'; -import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; - -before(() => { - login(); -}); - -const THREAT_INTELLIGENCE = '/app/security/threat_intelligence/indicators'; - -describe('Indicators', () => { - before(() => { - esArchiverLoad('threat_intelligence'); - }); - after(() => { - esArchiverUnload('threat_intelligence'); - }); - - describe('Indicators query bar interaction', () => { - before(() => { - cy.visit(THREAT_INTELLIGENCE); - - selectRange(); - }); - - it('should filter in and out values when clicking in an indicators table cell', () => { - cy.get(INDICATOR_TYPE_CELL).first().trigger('mouseover'); - cy.get(FILTER_IN_COMPONENT).should('exist'); - cy.get(FILTER_OUT_COMPONENT).should('exist'); - }); - - it('should filter in and out values when clicking in an indicators flyout table action column', () => { - cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true }); - cy.get(FILTER_OUT_BUTTON).should('exist'); - cy.get(FILTER_IN_BUTTON).should('exist').first().click(); - cy.get(FLYOUT_CLOSE_BUTTON).should('exist').click(); - cy.get(KQL_FILTER).should('exist'); - - cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true }); - cy.get(FILTER_IN_BUTTON).should('exist'); - cy.get(FILTER_OUT_BUTTON).should('exist').first().click(); - cy.get(FLYOUT_CLOSE_BUTTON).should('exist').click(); - cy.get(KQL_FILTER).should('exist'); - }); - }); -}); diff --git a/x-pack/plugins/threat_intelligence/cypress/screens/indicators.ts b/x-pack/plugins/threat_intelligence/cypress/screens/indicators.ts index f6e304467aea5f..b9f982105696e2 100644 --- a/x-pack/plugins/threat_intelligence/cypress/screens/indicators.ts +++ b/x-pack/plugins/threat_intelligence/cypress/screens/indicators.ts @@ -49,23 +49,55 @@ export const FIELD_BROWSER_MODAL_SOURCE_CHECKBOX = `[data-test-subj="field-_sour export const FIELD_BROWSER_CLOSE = `[data-test-subj="close"]`; -export const BARCHART_TIMELINE_BUTTON = '[data-test-subj="tiTimelineButton"]'; +export const BARCHART_POPOVER_BUTTON = '[data-test-subj="tiBarchartPopoverButton"]'; + +export const BARCHART_TIMELINE_BUTTON = '[data-test-subj="tiBarchartTimelineButton"]'; + +export const BARCHART_FILTER_IN_BUTTON = '[data-test-subj="tiBarchartFilterInButton"]'; + +export const BARCHART_FILTER_OUT_BUTTON = '[data-test-subj="tiBarchartFilterOutButton"]'; export const INDICATORS_TABLE_CELL_TIMELINE_BUTTON = '[data-test-subj="tiIndicatorsTableCellTimelineButton"]'; -export const FLYOUT_TABLE_ROW_TIMELINE_BUTTON = '[data-test-subj="tiFlyoutTableRowTimelineButton"]'; +export const INDICATORS_TABLE_CELL_FILTER_IN_BUTTON = + '[data-test-subj="tiIndicatorsTableCellFilterInButton"]'; -export const UNTITLED_TIMELINE_BUTTON = '[data-test-subj="flyoutOverlay"]'; +export const INDICATORS_TABLE_CELL_FILTER_OUT_BUTTON = + '[data-test-subj="tiIndicatorsTableCellFilterOutButton"]'; -export const TIMELINE_DRAGGABLE_ITEM = '[data-test-subj="providerContainer"]'; +export const FLYOUT_OVERVIEW_TAB_TABLE_ROW_TIMELINE_BUTTON = + '[data-test-subj="tiFlyoutOverviewTableRowTimelineButton"]'; + +export const FLYOUT_OVERVIEW_TAB_TABLE_ROW_FILTER_IN_BUTTON = + '[data-test-subj="tiFlyoutOverviewTableRowFilterInButton"]'; + +export const FLYOUT_OVERVIEW_TAB_TABLE_ROW_FILTER_OUT_BUTTON = + '[data-test-subj="tiFlyoutOverviewTableRowFilterOutButton"]'; -export const FILTER_IN_BUTTON = '[data-test-subj="tiFilterInIcon"]'; +export const FLYOUT_OVERVIEW_TAB_BLOCKS_ITEM = + '[data-test-subj="tiFlyoutOverviewHighLevelBlocksItem"]'; -export const FILTER_OUT_BUTTON = '[data-test-subj="tiFilterOutIcon"]'; +export const FLYOUT_OVERVIEW_TAB_BLOCKS_TIMELINE_BUTTON = + '[data-test-subj="tiFlyoutOverviewHighLevelBlocksTimelineButton"]'; -export const FILTER_IN_COMPONENT = '[data-test-subj="tiFilterInComponent"]'; +export const FLYOUT_OVERVIEW_TAB_BLOCKS_FILTER_IN_BUTTON = + '[data-test-subj="tiFlyoutOverviewHighLevelBlocksFilterInButton"]'; -export const FILTER_OUT_COMPONENT = '[data-test-subj="tiFilterOutComponent"]'; +export const FLYOUT_OVERVIEW_TAB_BLOCKS_FILTER_OUT_BUTTON = + '[data-test-subj="tiFlyoutOverviewHighLevelBlocksFilterOutButton"]'; + +export const FLYOUT_TABLE_TAB_ROW_TIMELINE_BUTTON = + '[data-test-subj="tiFlyoutTableTabRowTimelineButton"]'; + +export const FLYOUT_TABLE_TAB_ROW_FILTER_IN_BUTTON = + '[data-test-subj="tiFlyoutTableTabRowFilterInButton"]'; + +export const FLYOUT_TABLE_TAB_ROW_FILTER_OUT_BUTTON = + '[data-test-subj="tiFlyoutTableTabRowFilterOutButton"]'; + +export const UNTITLED_TIMELINE_BUTTON = '[data-test-subj="flyoutOverlay"]'; + +export const TIMELINE_DRAGGABLE_ITEM = '[data-test-subj="providerContainer"]'; export const KQL_FILTER = '[id="popoverFor_filter0"]'; diff --git a/x-pack/plugins/threat_intelligence/cypress/support/index.js b/x-pack/plugins/threat_intelligence/cypress/support/e2e.js similarity index 100% rename from x-pack/plugins/threat_intelligence/cypress/support/index.js rename to x-pack/plugins/threat_intelligence/cypress/support/e2e.js diff --git a/x-pack/plugins/threat_intelligence/cypress/tasks/login.ts b/x-pack/plugins/threat_intelligence/cypress/tasks/login.ts index 2df7b88f1607b2..f33034dccb9c5e 100644 --- a/x-pack/plugins/threat_intelligence/cypress/tasks/login.ts +++ b/x-pack/plugins/threat_intelligence/cypress/tasks/login.ts @@ -11,7 +11,12 @@ import type { UrlObject } from 'url'; import * as yaml from 'js-yaml'; import type { ROLES } from './privileges'; -import { hostDetailsUrl, LOGOUT_URL } from './navigation'; + +const LOGIN_API_ENDPOINT = '/internal/security/login'; +const LOGOUT_URL = '/logout'; + +export const hostDetailsUrl = (hostName: string) => + `/app/security/hosts/${hostName}/authentications`; /** * Credentials in the `kibana.dev.yml` config file will be used to authenticate @@ -43,11 +48,6 @@ const ELASTICSEARCH_USERNAME = 'ELASTICSEARCH_USERNAME'; */ const ELASTICSEARCH_PASSWORD = 'ELASTICSEARCH_PASSWORD'; -/** - * The Kibana server endpoint used for authentication - */ -const LOGIN_API_ENDPOINT = '/internal/security/login'; - /** * cy.visit will default to the baseUrl which uses the default kibana test user * This function will override that functionality in cy.visit by building the baseUrl diff --git a/x-pack/plugins/threat_intelligence/cypress/tasks/navigation.ts b/x-pack/plugins/threat_intelligence/cypress/tasks/navigation.ts deleted file mode 100644 index 741a2cf761e8c4..00000000000000 --- a/x-pack/plugins/threat_intelligence/cypress/tasks/navigation.ts +++ /dev/null @@ -1,20 +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. - */ - -export const INTEGRATIONS = 'app/integrations#/'; -export const FLEET = 'app/fleet/'; -export const LOGIN_API_ENDPOINT = '/internal/security/login'; -export const LOGOUT_API_ENDPOINT = '/api/security/logout'; -export const LOGIN_URL = '/login'; -export const LOGOUT_URL = '/logout'; - -export const hostDetailsUrl = (hostName: string) => - `/app/security/hosts/${hostName}/authentications`; - -export const navigateTo = (page: string) => { - cy.visit(page); -}; diff --git a/x-pack/plugins/threat_intelligence/cypress/tasks/select_range.ts b/x-pack/plugins/threat_intelligence/cypress/tasks/select_range.ts index 8bf94c7f920ee2..aa654a7c300d32 100644 --- a/x-pack/plugins/threat_intelligence/cypress/tasks/select_range.ts +++ b/x-pack/plugins/threat_intelligence/cypress/tasks/select_range.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { FIELD_BROWSER, TIME_RANGE_PICKER } from '../screens/indicators'; +import { TIME_RANGE_PICKER } from '../screens/indicators'; export const selectRange = () => { - cy.get(FIELD_BROWSER); + cy.get(`[data-test-subj="indicatorsTableEmptyState"]`); cy.get(TIME_RANGE_PICKER).first().click({ force: true }); cy.get('[aria-label="Time unit"]').select('y'); diff --git a/x-pack/plugins/threat_intelligence/package.json b/x-pack/plugins/threat_intelligence/package.json index 1af856fcdf6164..ebbe810a6629c3 100644 --- a/x-pack/plugins/threat_intelligence/package.json +++ b/x-pack/plugins/threat_intelligence/package.json @@ -3,21 +3,16 @@ "name": "threat_intelligence", "scripts": { "cypress": "../../../node_modules/.bin/cypress", - "cypress:open": "yarn cypress open --config-file ./cypress/cypress.json", - "cypress:open:ccs": "yarn cypress:open --config integrationFolder=./cypress/ccs_integration", + "cypress:open": "yarn cypress open --config-file ./cypress/cypress.config.ts", "cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/threat_intelligence_cypress/visual_config.ts", - "cypress:open:upgrade": "yarn cypress:open --config integrationFolder=./cypress/upgrade_integration", - "cypress:run": "yarn cypress:run:reporter --browser chrome --spec './cypress/integration/**/*.spec.ts'; status=$?; yarn junit:merge && exit $status", - "cypress:run:spec": "yarn cypress:run:reporter --browser chrome --spec ${SPEC_LIST:-'./cypress/integration/**/*.spec.ts'}; status=$?; yarn junit:merge && exit $status", - "cypress:run:cases": "yarn cypress:run:reporter --browser chrome --spec './cypress/integration/cases/*.spec.ts'; status=$?; yarn junit:merge && exit $status", - "cypress:run:firefox": "yarn cypress:run:reporter --browser firefox --spec './cypress/integration/**/*.spec.ts'; status=$?; yarn junit:merge && exit $status", - "cypress:run:reporter": "yarn cypress run --config-file ./cypress/cypress.json --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json", - "cypress:run:respops": "yarn cypress:run:reporter --browser chrome --spec ./cypress/integration/detection_alerts/*.spec.ts,./cypress/integration/detection_rules/*.spec.ts,./cypress/integration/exceptions/*.spec.ts; status=$?; yarn junit:merge && exit $status", - "cypress:run:ccs": "yarn cypress:run:reporter --browser chrome --config integrationFolder=./cypress/ccs_integration; status=$?; yarn junit:merge && exit $status", + "cypress:run": "yarn cypress:run:reporter --browser chrome --spec './cypress/e2e/**/*.cy.ts'; status=$?; yarn junit:merge && exit $status", + "cypress:run:spec": "yarn cypress:run:reporter --browser chrome --spec ${SPEC_LIST:-'./cypress/e2e/**/*.cy.ts'}; status=$?; yarn junit:merge && exit $status", + "cypress:run:cases": "yarn cypress:run:reporter --browser chrome --spec './cypress/e2e/cases/*.cy.ts'; status=$?; yarn junit:merge && exit $status", + "cypress:run:firefox": "yarn cypress:run:reporter --browser firefox --spec './cypress/e2e/**/*.cy.ts'; status=$?; yarn junit:merge && exit $status", + "cypress:run:reporter": "yarn cypress run --config-file ./cypress/cypress.config.ts --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json", + "cypress:run:respops": "yarn cypress:run:reporter --browser chrome --spec ./cypress/e2e/detection_alerts/*.cy.ts,./cypress/e2e/detection_rules/*.cy.ts,./cypress/e2e/exceptions/*.cy.ts; status=$?; yarn junit:merge && exit $status", "cypress:run-as-ci": "node --max-old-space-size=2048 ../../../scripts/functional_tests --config ../../test/threat_intelligence_cypress/cli_config_parallel.ts", "cypress:run-as-ci:firefox": "node --max-old-space-size=2048 ../../../scripts/functional_tests --config ../../test/threat_intelligence_cypress/config.firefox.ts", - "cypress:run:upgrade": "yarn cypress:run:reporter --browser chrome --config integrationFolder=./cypress/upgrade_integration", - "cypress:run:upgrade:old": "yarn cypress:run:reporter --browser chrome --config integrationFolder=./cypress/upgrade_integration --spec ./cypress/upgrade_integration/threat_hunting/**/*.spec.ts,./cypress/upgrade_integration/detections/**/custom_query_rule.spec.ts; status=$?; yarn junit:merge && exit $status", "junit:merge": "../../../node_modules/.bin/mochawesome-merge ../../../target/kibana-threat-intelligence/cypress/results/mochawesome*.json > ../../../target/kibana-threat-intelligence/cypress/results/output.json && ../../../node_modules/.bin/marge ../../../target/kibana-threat-intelligence/cypress/results/output.json --reportDir ../../../target/kibana-threat-intelligence/cypress/results && mkdir -p ../../../target/junit && cp ../../../target/kibana-threat-intelligence/cypress/results/*.xml ../../../target/junit/" } } diff --git a/x-pack/plugins/threat_intelligence/public/common/mocks/mock_field_type_map.ts b/x-pack/plugins/threat_intelligence/public/common/mocks/mock_field_type_map.ts index 39a03139f30a4e..90c8da120501e2 100644 --- a/x-pack/plugins/threat_intelligence/public/common/mocks/mock_field_type_map.ts +++ b/x-pack/plugins/threat_intelligence/public/common/mocks/mock_field_type_map.ts @@ -5,10 +5,12 @@ * 2.0. */ +import { FieldTypesContextValue } from '../../containers/field_types_provider'; + /** * Mock to map an indicator field to its type. */ -export const generateFieldTypeMap = (): { [id: string]: string } => ({ +export const generateFieldTypeMap = (): FieldTypesContextValue => ({ '@timestamp': 'date', 'threat.indicator.ip': 'ip', 'threat.indicator.first_seen': 'date', diff --git a/x-pack/plugins/threat_intelligence/public/common/mocks/story_providers.tsx b/x-pack/plugins/threat_intelligence/public/common/mocks/story_providers.tsx index 6da13f89d08873..78c064f72b449c 100644 --- a/x-pack/plugins/threat_intelligence/public/common/mocks/story_providers.tsx +++ b/x-pack/plugins/threat_intelligence/public/common/mocks/story_providers.tsx @@ -5,13 +5,21 @@ * 2.0. */ -import React, { ReactNode, VFC } from 'react'; +import React, { FC, ReactNode, VFC } from 'react'; import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { IUiSettingsClient } from '@kbn/core/public'; +import { CoreStart, IUiSettingsClient } from '@kbn/core/public'; import { TimelinesUIStart } from '@kbn/timelines-plugin/public'; +import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; +import { mockIndicatorsFiltersContext } from './mock_indicators_filters_context'; import { SecuritySolutionContext } from '../../containers/security_solution_context'; import { getSecuritySolutionContextMock } from './mock_security_context'; +import { IndicatorsFiltersContext } from '../../modules/indicators/context'; +import { FieldTypesContext } from '../../containers/field_types_provider'; +import { generateFieldTypeMap } from './mock_field_type_map'; +import { mockUiSettingsService } from './mock_kibana_ui_settings_service'; +import { mockKibanaTimelinesService } from './mock_kibana_timelines_service'; +import { mockTriggersActionsUiService } from './mock_kibana_triggers_actions_ui_service'; export interface KibanaContextMock { /** @@ -30,29 +38,56 @@ export interface KibanaContextMock { export interface StoryProvidersComponentProps { /** - * Used to generate a new KibanaReactContext (using {@link createKibanaReactContext}) + * Extend / override mock services specified in {@link defaultServices} to create KibanaReactContext (using {@link createKibanaReactContext}). This is optional. */ - kibana: KibanaContextMock; + kibana?: KibanaContextMock; /** * Component(s) to be displayed inside */ children: ReactNode; } +const securityLayout = { + getPluginWrapper: + (): FC => + ({ children }) => + <div>{children}</div>, +}; + +const defaultServices = { + uiSettings: mockUiSettingsService(), + timelines: mockKibanaTimelinesService, + triggersActionsUi: mockTriggersActionsUiService, + storage: { + set: () => {}, + get: () => {}, + }, +} as unknown as CoreStart; + /** * Helper functional component used in Storybook stories. * Wraps the story with our {@link SecuritySolutionContext} and KibanaReactContext. */ export const StoryProvidersComponent: VFC<StoryProvidersComponentProps> = ({ children, - kibana, + kibana = {}, }) => { - const KibanaReactContext = createKibanaReactContext(kibana); + const KibanaReactContext = createKibanaReactContext({ + ...defaultServices, + ...kibana, + securityLayout, + }); const securitySolutionContextMock = getSecuritySolutionContextMock(); return ( - <SecuritySolutionContext.Provider value={securitySolutionContextMock}> - <KibanaReactContext.Provider>{children}</KibanaReactContext.Provider> - </SecuritySolutionContext.Provider> + <EuiThemeProvider> + <FieldTypesContext.Provider value={generateFieldTypeMap()}> + <SecuritySolutionContext.Provider value={securitySolutionContextMock}> + <IndicatorsFiltersContext.Provider value={mockIndicatorsFiltersContext}> + <KibanaReactContext.Provider>{children}</KibanaReactContext.Provider> + </IndicatorsFiltersContext.Provider> + </SecuritySolutionContext.Provider> + </FieldTypesContext.Provider> + </EuiThemeProvider> ); }; diff --git a/x-pack/plugins/threat_intelligence/public/common/mocks/test_providers.tsx b/x-pack/plugins/threat_intelligence/public/common/mocks/test_providers.tsx index 8648169dfdb4a2..d3a94bbcce96ee 100644 --- a/x-pack/plugins/threat_intelligence/public/common/mocks/test_providers.tsx +++ b/x-pack/plugins/threat_intelligence/public/common/mocks/test_providers.tsx @@ -15,6 +15,7 @@ import type { IStorage } from '@kbn/kibana-utils-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { createTGridMocks } from '@kbn/timelines-plugin/public/mock'; +import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; import { KibanaContext } from '../../hooks/use_kibana'; import { SecuritySolutionPluginContext } from '../../types'; import { getSecuritySolutionContextMock } from './mock_security_context'; @@ -22,6 +23,8 @@ import { mockUiSetting } from './mock_kibana_ui_settings_service'; import { SecuritySolutionContext } from '../../containers/security_solution_context'; import { IndicatorsFiltersContext } from '../../modules/indicators/context'; import { mockIndicatorsFiltersContext } from './mock_indicators_filters_context'; +import { FieldTypesContext } from '../../containers/field_types_provider'; +import { generateFieldTypeMap } from './mock_field_type_map'; export const localStorageMock = (): IStorage => { let store: Record<string, unknown> = {}; @@ -122,15 +125,19 @@ export const mockedServices = { }; export const TestProvidersComponent: FC = ({ children }) => ( - <SecuritySolutionContext.Provider value={mockSecurityContext}> - <KibanaContext.Provider value={{ services: mockedServices } as any}> - <I18nProvider> - <IndicatorsFiltersContext.Provider value={mockIndicatorsFiltersContext}> - {children} - </IndicatorsFiltersContext.Provider> - </I18nProvider> - </KibanaContext.Provider> - </SecuritySolutionContext.Provider> + <FieldTypesContext.Provider value={generateFieldTypeMap()}> + <EuiThemeProvider> + <SecuritySolutionContext.Provider value={mockSecurityContext}> + <KibanaContext.Provider value={{ services: mockedServices } as any}> + <I18nProvider> + <IndicatorsFiltersContext.Provider value={mockIndicatorsFiltersContext}> + {children} + </IndicatorsFiltersContext.Provider> + </I18nProvider> + </KibanaContext.Provider> + </SecuritySolutionContext.Provider> + </EuiThemeProvider> + </FieldTypesContext.Provider> ); export type MockedSearch = jest.Mocked<typeof mockedServices.data.search>; diff --git a/x-pack/plugins/threat_intelligence/public/components/layout/layout.stories.tsx b/x-pack/plugins/threat_intelligence/public/components/layout/layout.stories.tsx index e6b73615c61b23..5776af28466883 100644 --- a/x-pack/plugins/threat_intelligence/public/components/layout/layout.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/components/layout/layout.stories.tsx @@ -9,17 +9,22 @@ import React from 'react'; import { Story } from '@storybook/react'; import { EuiText } from '@elastic/eui'; import { DefaultPageLayout } from './layout'; +import { StoryProvidersComponent } from '../../common/mocks/story_providers'; export default { - component: DefaultPageLayout, title: 'DefaultPageLayout', + component: DefaultPageLayout, }; export const Default: Story<void> = () => { const title = 'Title with border below'; const children = <EuiText>Content with border above</EuiText>; - return <DefaultPageLayout pageTitle={title} children={children} />; + return ( + <StoryProvidersComponent> + <DefaultPageLayout pageTitle={title} children={children} /> + </StoryProvidersComponent> + ); }; export const NoBorder: Story<void> = () => { @@ -27,5 +32,9 @@ export const NoBorder: Story<void> = () => { const border = false; const children = <EuiText>Content without border</EuiText>; - return <DefaultPageLayout pageTitle={title} border={border} children={children} />; + return ( + <StoryProvidersComponent> + <DefaultPageLayout pageTitle={title} border={border} children={children} /> + </StoryProvidersComponent> + ); }; diff --git a/x-pack/plugins/threat_intelligence/public/containers/field_types_provider.tsx b/x-pack/plugins/threat_intelligence/public/containers/field_types_provider.tsx new file mode 100644 index 00000000000000..050ecb4a3fe102 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/containers/field_types_provider.tsx @@ -0,0 +1,33 @@ +/* + * 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, { createContext, useMemo } from 'react'; +import { FC } from 'react'; +import { useSourcererDataView } from '../modules/indicators/hooks/use_sourcerer_data_view'; + +export type FieldTypesContextValue = Record<string, string | undefined>; + +export const FieldTypesContext = createContext<FieldTypesContextValue | undefined>({}); + +/** + * Exposes mapped field types for threat intel shared use + */ +export const FieldTypesProvider: FC = ({ children }) => { + const { indexPattern } = useSourcererDataView(); + + // field name to field type map to allow the cell_renderer to format dates + const fieldTypes: FieldTypesContextValue = useMemo( + () => + indexPattern.fields.reduce((acc, field) => { + acc[field.name] = field.type; + return acc; + }, {} as FieldTypesContextValue), + [indexPattern.fields] + ); + + return <FieldTypesContext.Provider value={fieldTypes}>{children}</FieldTypesContext.Provider>; +}; diff --git a/x-pack/plugins/threat_intelligence/public/hooks/use_field_types.ts b/x-pack/plugins/threat_intelligence/public/hooks/use_field_types.ts new file mode 100644 index 00000000000000..66ae2456e2dcf2 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/hooks/use_field_types.ts @@ -0,0 +1,13 @@ +/* + * 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 { useContext } from 'react'; +import { FieldTypesContext } from '../containers/field_types_provider'; + +export const useFieldTypes = () => { + return useContext(FieldTypesContext) || {}; +}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/empty_page/empty_page.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/empty_page/empty_page.stories.tsx index d284fe24052bcc..bc298ef95eecf7 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/empty_page/empty_page.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/empty_page/empty_page.stories.tsx @@ -6,9 +6,8 @@ */ import React from 'react'; -import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; -import { CoreStart } from '@kbn/core/public'; import { EmptyPage } from '.'; +import { StoryProvidersComponent } from '../../common/mocks/story_providers'; export default { component: BasicEmptyPage, @@ -16,7 +15,7 @@ export default { }; export function BasicEmptyPage() { - const KibanaReactContext = createKibanaReactContext({ + const kibana = { http: { basePath: { get: () => '', @@ -29,10 +28,11 @@ export function BasicEmptyPage() { }, }, }, - } as unknown as Partial<CoreStart>); + }; + return ( - <KibanaReactContext.Provider> + <StoryProvidersComponent kibana={kibana as any}> <EmptyPage /> - </KibanaReactContext.Provider> + </StoryProvidersComponent> ); } diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_barchart_legend_action/indicator_barchart_legend_action.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_barchart_legend_action/indicator_barchart_legend_action.tsx new file mode 100644 index 00000000000000..e015e409ec2279 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_barchart_legend_action/indicator_barchart_legend_action.tsx @@ -0,0 +1,77 @@ +/* + * 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, { useState, VFC } from 'react'; +import { EuiButtonIcon, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; +import { ComponentType } from '../../../../../common/types/component_type'; +import { FilterIn } from '../../../query_bar/components/filter_in'; +import { FilterOut } from '../../../query_bar/components/filter_out'; +import { AddToTimeline } from '../../../timeline/components/add_to_timeline'; + +export const POPOVER_BUTTON_TEST_ID = 'tiBarchartPopoverButton'; +export const TIMELINE_BUTTON_TEST_ID = 'tiBarchartTimelineButton'; +export const FILTER_IN_BUTTON_TEST_ID = 'tiBarchartFilterInButton'; +export const FILTER_OUT_BUTTON_TEST_ID = 'tiBarchartFilterOutButton'; + +export interface IndicatorBarchartLegendActionProps { + /** + * Indicator + */ + data: string; + /** + * Indicator field selected in the IndicatorFieldSelector component, passed to the {@link AddToTimeline} to populate the timeline. + */ + field: string; +} + +export const IndicatorBarchartLegendAction: VFC<IndicatorBarchartLegendActionProps> = ({ + data, + field, +}) => { + const [isPopoverOpen, setPopover] = useState(false); + + const popoverItems = [ + <FilterIn + data={data} + field={field} + type={ComponentType.ContextMenu} + data-test-subj={FILTER_IN_BUTTON_TEST_ID} + />, + <FilterOut + data={data} + field={field} + type={ComponentType.ContextMenu} + data-test-subj={FILTER_OUT_BUTTON_TEST_ID} + />, + <AddToTimeline + data={data} + field={field} + type={ComponentType.ContextMenu} + data-test-subj={TIMELINE_BUTTON_TEST_ID} + />, + ]; + + return ( + <EuiPopover + data-test-subj={POPOVER_BUTTON_TEST_ID} + button={ + <EuiButtonIcon + iconType="boxesHorizontal" + iconSize="s" + size="xs" + onClick={() => setPopover(!isPopoverOpen)} + /> + } + isOpen={isPopoverOpen} + closePopover={() => setPopover(false)} + panelPaddingSize="none" + anchorPosition="downLeft" + > + <EuiContextMenuPanel size="s" items={popoverItems} /> + </EuiPopover> + ); +}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/indicator_field.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/indicator_field.stories.tsx deleted file mode 100644 index 467ff2ea16cf36..00000000000000 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/indicator_field.stories.tsx +++ /dev/null @@ -1,53 +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 React from 'react'; -import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; -import { generateFieldTypeMap } from '../../../../common/mocks/mock_field_type_map'; -import { mockUiSettingsService } from '../../../../common/mocks/mock_kibana_ui_settings_service'; -import { generateMockIndicator } from '../../../../../common/types/indicator'; -import { IndicatorField } from './indicator_field'; - -export default { - component: IndicatorField, - title: 'IndicatorField', -}; - -const mockIndicator = generateMockIndicator(); - -const mockFieldTypesMap = generateFieldTypeMap(); - -export function Default() { - const mockField = 'threat.indicator.ip'; - - return ( - <IndicatorField indicator={mockIndicator} field={mockField} fieldTypesMap={mockFieldTypesMap} /> - ); -} - -export function IncorrectField() { - const mockField = 'abc'; - - return ( - <IndicatorField indicator={mockIndicator} field={mockField} fieldTypesMap={mockFieldTypesMap} /> - ); -} - -export function HandlesDates() { - const KibanaReactContext = createKibanaReactContext({ uiSettings: mockUiSettingsService() }); - const mockField = 'threat.indicator.first_seen'; - - return ( - <KibanaReactContext.Provider> - <IndicatorField - indicator={mockIndicator} - field={mockField} - fieldTypesMap={mockFieldTypesMap} - /> - </KibanaReactContext.Provider> - ); -} diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/index.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field_label/index.tsx similarity index 81% rename from x-pack/plugins/apm/ftr_e2e/cypress/support/index.ts rename to x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field_label/index.tsx index 48367848ed48f1..a2f2520c9541bd 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/index.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field_label/index.tsx @@ -5,5 +5,4 @@ * 2.0. */ -import './commands'; -// import './output_command_timings'; +export * from './indicator_field_label'; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field_label/indicator_field_label.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field_label/indicator_field_label.tsx new file mode 100644 index 00000000000000..64e85bc8c5d7e0 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field_label/indicator_field_label.tsx @@ -0,0 +1,71 @@ +/* + * 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, { VFC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { RawIndicatorFieldId } from '../../../../../common/types/indicator'; + +interface IndicatorFieldLabelProps { + field: string; +} + +/** + * Renders field label using i18n, or the field key if the translation is not available + */ +export const IndicatorFieldLabel: VFC<IndicatorFieldLabelProps> = ({ field }) => ( + <>{translateFieldLabel(field)}</> +); + +/** This translates the field name using kbn-i18n */ +export const translateFieldLabel = (field: string) => { + // This switch is necessary as i18n id cannot be dynamic, see: + // https://github.com/elastic/kibana/blob/main/src/dev/i18n/README.md + switch (field) { + case RawIndicatorFieldId.TimeStamp: { + return i18n.translate('xpack.threatIntelligence.field.@timestamp', { + defaultMessage: '@timestamp', + }); + } + case RawIndicatorFieldId.Name: { + return i18n.translate('xpack.threatIntelligence.field.threat.indicator.name', { + defaultMessage: 'Indicator', + }); + } + case RawIndicatorFieldId.Type: { + return i18n.translate('xpack.threatIntelligence.field.threat.indicator.type', { + defaultMessage: 'Indicator type', + }); + } + case RawIndicatorFieldId.Feed: { + return i18n.translate('xpack.threatIntelligence.field.threat.feed.name', { + defaultMessage: 'Feed', + }); + } + case RawIndicatorFieldId.FirstSeen: { + return i18n.translate('xpack.threatIntelligence.field.threat.indicator.first_seen', { + defaultMessage: 'First seen', + }); + } + case RawIndicatorFieldId.LastSeen: { + return i18n.translate('xpack.threatIntelligence.field.threat.indicator.last_seen', { + defaultMessage: 'Last seen', + }); + } + case RawIndicatorFieldId.Confidence: { + return i18n.translate('xpack.threatIntelligence.field.threat.indicator.confidence', { + defaultMessage: 'Confidence', + }); + } + case RawIndicatorFieldId.MarkingTLP: { + return i18n.translate('xpack.threatIntelligence.field.threat.indicator.marking.tlp', { + defaultMessage: 'TLP Marking', + }); + } + default: + return field; + } +}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/__snapshots__/indicator_field.test.tsx.snap b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field_value/__snapshots__/indicator_field.test.tsx.snap similarity index 100% rename from x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/__snapshots__/indicator_field.test.tsx.snap rename to x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field_value/__snapshots__/indicator_field.test.tsx.snap diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field_value/index.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field_value/index.tsx new file mode 100644 index 00000000000000..724caf3c752430 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field_value/index.tsx @@ -0,0 +1,8 @@ +/* + * 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 * from './indicator_field_value'; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field_value/indicator_field.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field_value/indicator_field.stories.tsx new file mode 100644 index 00000000000000..9548691e49ec51 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field_value/indicator_field.stories.tsx @@ -0,0 +1,40 @@ +/* + * 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 from 'react'; +import { StoryProvidersComponent } from '../../../../common/mocks/story_providers'; +import { generateMockIndicator } from '../../../../../common/types/indicator'; +import { IndicatorFieldValue } from './indicator_field_value'; + +export default { + component: IndicatorFieldValue, + title: 'IndicatorFieldValue', +}; + +const mockIndicator = generateMockIndicator(); + +export function Default() { + const mockField = 'threat.indicator.ip'; + + return <IndicatorFieldValue indicator={mockIndicator} field={mockField} />; +} + +export function IncorrectField() { + const mockField = 'abc'; + + return <IndicatorFieldValue indicator={mockIndicator} field={mockField} />; +} + +export function HandlesDates() { + const mockField = 'threat.indicator.first_seen'; + + return ( + <StoryProvidersComponent> + <IndicatorFieldValue indicator={mockIndicator} field={mockField} /> + </StoryProvidersComponent> + ); +} diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/indicator_field.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field_value/indicator_field.test.tsx similarity index 63% rename from x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/indicator_field.test.tsx rename to x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field_value/indicator_field.test.tsx index 0cd77b95b7b1a2..c695a2c4ebe847 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/indicator_field.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field_value/indicator_field.test.tsx @@ -7,39 +7,25 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { IndicatorField } from './indicator_field'; +import { IndicatorFieldValue } from './indicator_field_value'; import { generateMockIndicator } from '../../../../../common/types/indicator'; import { EMPTY_VALUE } from '../../../../../common/constants'; import { TestProvidersComponent } from '../../../../common/mocks/test_providers'; -import { generateFieldTypeMap } from '../../../../common/mocks/mock_field_type_map'; const mockIndicator = generateMockIndicator(); -const mockFieldTypesMap = generateFieldTypeMap(); describe('<IndicatorField />', () => { beforeEach(() => {}); it('should return non formatted value', () => { const mockField = 'threat.indicator.ip'; - const component = render( - <IndicatorField - indicator={mockIndicator} - field={mockField} - fieldTypesMap={mockFieldTypesMap} - /> - ); + const component = render(<IndicatorFieldValue indicator={mockIndicator} field={mockField} />); expect(component).toMatchSnapshot(); }); it(`should return ${EMPTY_VALUE}`, () => { const mockField = 'abc'; - const component = render( - <IndicatorField - indicator={mockIndicator} - field={mockField} - fieldTypesMap={mockFieldTypesMap} - /> - ); + const component = render(<IndicatorFieldValue indicator={mockIndicator} field={mockField} />); expect(component).toMatchSnapshot(); }); @@ -47,11 +33,7 @@ describe('<IndicatorField />', () => { const mockField = 'threat.indicator.first_seen'; const component = render( <TestProvidersComponent> - <IndicatorField - indicator={mockIndicator} - field={mockField} - fieldTypesMap={mockFieldTypesMap} - /> + <IndicatorFieldValue indicator={mockIndicator} field={mockField} /> </TestProvidersComponent> ); expect(component).toMatchSnapshot(); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/indicator_field.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field_value/indicator_field_value.tsx similarity index 80% rename from x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/indicator_field.tsx rename to x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field_value/indicator_field_value.tsx index 6ea779c28be295..c0b46cd1b44b0f 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/indicator_field.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field_value/indicator_field_value.tsx @@ -6,12 +6,13 @@ */ import React, { VFC } from 'react'; +import { useFieldTypes } from '../../../../hooks/use_field_types'; import { EMPTY_VALUE } from '../../../../../common/constants'; import { Indicator, RawIndicatorFieldId } from '../../../../../common/types/indicator'; import { DateFormatter } from '../../../../components/date_formatter'; import { unwrapValue } from '../../lib/unwrap_value'; -export interface IndicatorFieldProps { +export interface IndicatorFieldValueProps { /** * Indicator to display the field value from (see {@link Indicator}). */ @@ -20,19 +21,16 @@ export interface IndicatorFieldProps { * The field to get the indicator's value for. */ field: string; - /** - * An object to know what type the field is ('file', 'date', ...). - */ - fieldTypesMap: { [id: string]: string }; } /** * Takes an indicator object, a field and a field => type object to returns the correct value to display. * @returns If the type is a 'date', returns the {@link DateFormatter} component, else returns the value or {@link EMPTY_VALUE}. */ -export const IndicatorField: VFC<IndicatorFieldProps> = ({ indicator, field, fieldTypesMap }) => { +export const IndicatorFieldValue: VFC<IndicatorFieldValueProps> = ({ indicator, field }) => { + const fieldType = useFieldTypes()[field]; + const value = unwrapValue(indicator, field as RawIndicatorFieldId); - const fieldType = fieldTypesMap[field]; return fieldType === 'date' ? ( <DateFormatter date={value as string} /> ) : value ? ( diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_value_actions/index.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_value_actions/index.tsx new file mode 100644 index 00000000000000..1e268b953ef094 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_value_actions/index.tsx @@ -0,0 +1,8 @@ +/* + * 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 * from './indicator_value_actions'; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_value_actions/indicator_value_actions.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_value_actions/indicator_value_actions.tsx new file mode 100644 index 00000000000000..1fdc58f85cfcf6 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_value_actions/indicator_value_actions.tsx @@ -0,0 +1,68 @@ +/* + * 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 { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; +import React, { VFC } from 'react'; +import { EMPTY_VALUE } from '../../../../../common/constants'; +import { Indicator } from '../../../../../common/types/indicator'; +import { FilterIn } from '../../../query_bar/components/filter_in'; +import { FilterOut } from '../../../query_bar/components/filter_out'; +import { AddToTimeline } from '../../../timeline/components/add_to_timeline'; +import { getIndicatorFieldAndValue } from '../../lib/field_value'; + +export const TIMELINE_BUTTON_TEST_ID = 'TimelineButton'; +export const FILTER_IN_BUTTON_TEST_ID = 'FilterInButton'; +export const FILTER_OUT_BUTTON_TEST_ID = 'FilterOutButton'; + +interface IndicatorValueActions { + /** + * Indicator complete object. + */ + indicator: Indicator; + /** + * Indicator field used for the filter in/out and add to timeline feature. + */ + field: string; + /** + * Only used with `EuiDataGrid` (see {@link AddToTimelineButtonProps}). + */ + Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon; + /** + * Used for unit and e2e tests. + */ + ['data-test-subj']?: string; +} + +export const IndicatorValueActions: VFC<IndicatorValueActions> = ({ + indicator, + field, + Component, + ...props +}) => { + const { key, value } = getIndicatorFieldAndValue(indicator, field); + + if (!key || value === EMPTY_VALUE || !key) { + return null; + } + + const filterInTestId = `${props['data-test-subj']}${FILTER_IN_BUTTON_TEST_ID}`; + const filterOutTestId = `${props['data-test-subj']}${FILTER_OUT_BUTTON_TEST_ID}`; + const timelineTestId = `${props['data-test-subj']}${TIMELINE_BUTTON_TEST_ID}`; + + return ( + <> + <FilterIn as={Component} data={indicator} field={field} data-test-subj={filterInTestId} /> + <FilterOut as={Component} data={indicator} field={field} data-test-subj={filterOutTestId} /> + <AddToTimeline + as={Component} + data={indicator} + field={field} + data-test-subj={timelineTestId} + /> + </> + ); +}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart/indicators_barchart.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart/indicators_barchart.stories.tsx index d3884b79947268..465ef3bd1ab78f 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart/indicators_barchart.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart/indicators_barchart.stories.tsx @@ -9,8 +9,7 @@ import moment from 'moment'; import React from 'react'; import { Story } from '@storybook/react'; import { TimeRangeBounds } from '@kbn/data-plugin/common'; -import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; -import { CoreStart } from '@kbn/core/public'; +import { StoryProvidersComponent } from '../../../../common/mocks/story_providers'; import { mockKibanaTimelinesService } from '../../../../common/mocks/mock_kibana_timelines_service'; import { ChartSeries } from '../../hooks/use_aggregated_indicators'; import { IndicatorsBarChart } from './indicators_barchart'; @@ -58,38 +57,34 @@ const mockDateRange: TimeRangeBounds = { const mockField: string = 'threat.indicator.ip'; -const KibanaReactContext = createKibanaReactContext({ - timelines: mockKibanaTimelinesService, -} as unknown as CoreStart); - export default { component: IndicatorsBarChart, title: 'IndicatorsBarChart', }; export const Default: Story<void> = () => ( - <KibanaReactContext.Provider> + <StoryProvidersComponent kibana={{ timelines: mockKibanaTimelinesService }}> <IndicatorsBarChart indicators={mockIndicators} field={mockField} dateRange={mockDateRange} /> - </KibanaReactContext.Provider> + </StoryProvidersComponent> ); export const NoData: Story<void> = () => ( - <KibanaReactContext.Provider> + <StoryProvidersComponent kibana={{ timelines: mockKibanaTimelinesService }}> <IndicatorsBarChart indicators={[]} field={''} dateRange={mockDateRange} /> - </KibanaReactContext.Provider> + </StoryProvidersComponent> ); export const CustomHeight: Story<void> = () => { const mockHeight = '500px'; return ( - <KibanaReactContext.Provider> + <StoryProvidersComponent kibana={{ timelines: mockKibanaTimelinesService }}> <IndicatorsBarChart indicators={mockIndicators} field={mockField} dateRange={mockDateRange} height={mockHeight} /> - </KibanaReactContext.Provider> + </StoryProvidersComponent> ); }; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart/indicators_barchart.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart/indicators_barchart.tsx index f3c352c6296d03..75d731b23c3b5b 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart/indicators_barchart.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart/indicators_barchart.tsx @@ -9,12 +9,10 @@ import React, { VFC } from 'react'; import { Axis, BarSeries, Chart, Position, ScaleType, Settings } from '@elastic/charts'; import { EuiThemeProvider } from '@elastic/eui'; import { TimeRangeBounds } from '@kbn/data-plugin/common'; -import { AddToTimeline } from '../../../timeline/components/add_to_timeline'; +import { IndicatorBarchartLegendAction } from '../indicator_barchart_legend_action/indicator_barchart_legend_action'; import { barChartTimeAxisLabelFormatter } from '../../../../common/utils/dates'; import { ChartSeries } from '../../hooks/use_aggregated_indicators'; -export const TIMELINE_BUTTON_TEST_ID = 'tiTimelineButton'; - const ID = 'tiIndicator'; const DEFAULT_CHART_HEIGHT = '200px'; const DEFAULT_CHART_WIDTH = '100%'; @@ -54,9 +52,7 @@ export const IndicatorsBarChart: VFC<IndicatorsBarChartProps> = ({ showLegend showLegendExtra legendPosition={Position.Right} - legendAction={({ label }) => ( - <AddToTimeline data={label} field={field} testId={TIMELINE_BUTTON_TEST_ID} /> - )} + legendAction={({ label }) => <IndicatorBarchartLegendAction field={field} data={label} />} /> <Axis id={`${ID}TimeAxis`} diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/components/indicator_empty_prompt/index.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/components/indicator_empty_prompt/index.tsx new file mode 100644 index 00000000000000..a2b896781739c3 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/components/indicator_empty_prompt/index.tsx @@ -0,0 +1,8 @@ +/* + * 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 * from './indicator_empty_prompt'; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/components/indicator_empty_prompt/indicator_empty_prompt.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/components/indicator_empty_prompt/indicator_empty_prompt.stories.tsx new file mode 100644 index 00000000000000..56d66781d187d8 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/components/indicator_empty_prompt/indicator_empty_prompt.stories.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Story } from '@storybook/react'; +import { StoryProvidersComponent } from '../../../../../../common/mocks/story_providers'; +import { IndicatorEmptyPrompt } from './indicator_empty_prompt'; + +export default { + component: IndicatorEmptyPrompt, + title: 'IndicatorEmptyPrompt', +}; + +export const Default: Story<void> = () => { + return ( + <StoryProvidersComponent> + <IndicatorEmptyPrompt /> + </StoryProvidersComponent> + ); +}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/components/indicator_empty_prompt/indicator_empty_prompt.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/components/indicator_empty_prompt/indicator_empty_prompt.tsx new file mode 100644 index 00000000000000..0edf3e67f3c03b --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/components/indicator_empty_prompt/indicator_empty_prompt.tsx @@ -0,0 +1,37 @@ +/* + * 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 { EuiEmptyPrompt } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; +import { VFC } from 'react'; + +export const EMPTY_PROMPT_TEST_ID = 'indicatorEmptyPrompt'; + +export const IndicatorEmptyPrompt: VFC = () => ( + <EuiEmptyPrompt + iconType="alert" + color="danger" + title={ + <h2> + <FormattedMessage + id="xpack.threatIntelligence.indicator.flyoutTable.errorMessageTitle" + defaultMessage="Unable to display indicator information" + /> + </h2> + } + body={ + <p> + <FormattedMessage + id="xpack.threatIntelligence.indicator.flyoutTable.errorMessageBody" + defaultMessage="There was an error displaying the indicator fields and values." + /> + </p> + } + data-test-subj={EMPTY_PROMPT_TEST_ID} + /> +); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/components/indicator_fields_table/index.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/components/indicator_fields_table/index.tsx new file mode 100644 index 00000000000000..9252945c8f5520 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/components/indicator_fields_table/index.tsx @@ -0,0 +1,8 @@ +/* + * 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 * from './indicator_fields_table'; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/components/indicator_fields_table/indicator_fields_table.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/components/indicator_fields_table/indicator_fields_table.stories.tsx new file mode 100644 index 00000000000000..c867eda97389f5 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/components/indicator_fields_table/indicator_fields_table.stories.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mockIndicatorsFiltersContext } from '../../../../../../common/mocks/mock_indicators_filters_context'; +import { IndicatorFieldsTable } from './indicator_fields_table'; +import { generateMockIndicator } from '../../../../../../../common/types/indicator'; +import { StoryProvidersComponent } from '../../../../../../common/mocks/story_providers'; +import { IndicatorsFiltersContext } from '../../../../context'; + +export default { + component: IndicatorFieldsTable, + title: 'IndicatorFieldsTable', +}; + +export function WithIndicators() { + const indicator = generateMockIndicator(); + + return ( + <StoryProvidersComponent> + <IndicatorsFiltersContext.Provider value={mockIndicatorsFiltersContext}> + <IndicatorFieldsTable + fields={['threat.indicator.type']} + indicator={indicator} + search={false} + /> + </IndicatorsFiltersContext.Provider> + </StoryProvidersComponent> + ); +} diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/components/indicator_fields_table/indicator_fields_table.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/components/indicator_fields_table/indicator_fields_table.tsx new file mode 100644 index 00000000000000..13bb919009bae7 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/components/indicator_fields_table/indicator_fields_table.tsx @@ -0,0 +1,63 @@ +/* + * 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 { EuiBasicTableColumn, EuiInMemoryTable, EuiInMemoryTableProps } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useMemo, VFC } from 'react'; +import { Indicator } from '../../../../../../../common/types/indicator'; +import { IndicatorFieldValue } from '../../../indicator_field_value'; +import { IndicatorValueActions } from '../../../indicator_value_actions'; + +export interface IndicatorFieldsTableProps { + fields: string[]; + indicator: Indicator; + search: EuiInMemoryTableProps<string>['search']; + ['data-test-subj']?: string; +} + +export const IndicatorFieldsTable: VFC<IndicatorFieldsTableProps> = ({ + fields, + indicator, + ...rest +}) => { + const columns = useMemo( + () => + [ + { + name: ( + <FormattedMessage + id="xpack.threatIntelligence.indicator.fieldsTable.fieldColumnLabel" + defaultMessage="Field" + /> + ), + render: (field: string) => field, + }, + { + name: ( + <FormattedMessage + id="xpack.threatIntelligence.indicator.fieldsTable.valueColumnLabel" + defaultMessage="Value" + /> + ), + render: (field: string) => <IndicatorFieldValue indicator={indicator} field={field} />, + }, + { + actions: [ + { + render: (field: string) => ( + <IndicatorValueActions field={field} indicator={indicator} {...rest} /> + ), + width: '72px', + }, + ], + }, + ] as Array<EuiBasicTableColumn<string>>, + [indicator, rest] + ); + + return <EuiInMemoryTable items={fields} columns={columns} sorting={true} {...rest} />; +}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.stories.tsx index 5076b1649635e2..ec87259c90a58a 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.stories.tsx @@ -10,7 +10,6 @@ import { Story } from '@storybook/react'; import { CoreStart } from '@kbn/core/public'; import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; import { mockIndicatorsFiltersContext } from '../../../../common/mocks/mock_indicators_filters_context'; -import { generateFieldTypeMap } from '../../../../common/mocks/mock_field_type_map'; import { mockUiSettingsService } from '../../../../common/mocks/mock_kibana_ui_settings_service'; import { mockKibanaTimelinesService } from '../../../../common/mocks/mock_kibana_timelines_service'; import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator'; @@ -30,14 +29,12 @@ const KibanaReactContext = createKibanaReactContext(coreMock); export const Default: Story<void> = () => { const mockIndicator: Indicator = generateMockIndicator(); - const mockFieldTypesMap = generateFieldTypeMap(); return ( <KibanaReactContext.Provider> <IndicatorsFiltersContext.Provider value={mockIndicatorsFiltersContext}> <IndicatorsFlyout indicator={mockIndicator} - fieldTypesMap={mockFieldTypesMap} closeFlyout={() => window.alert('Closing flyout')} /> </IndicatorsFiltersContext.Provider> @@ -51,7 +48,6 @@ export const EmptyIndicator: Story<void> = () => { <IndicatorsFiltersContext.Provider value={mockIndicatorsFiltersContext}> <IndicatorsFlyout indicator={{ fields: {} } as Indicator} - fieldTypesMap={{}} closeFlyout={() => window.alert('Closing flyout')} /> </IndicatorsFiltersContext.Provider> diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.test.tsx index 4ad5c0e5f038a4..08add53bafcb10 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.test.tsx @@ -6,66 +6,57 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { cleanup, render, screen } from '@testing-library/react'; import { IndicatorsFlyout, SUBTITLE_TEST_ID, TITLE_TEST_ID } from './indicators_flyout'; -import { generateMockIndicator, RawIndicatorFieldId } from '../../../../../common/types/indicator'; -import { EMPTY_VALUE } from '../../../../../common/constants'; -import { dateFormatter } from '../../../../common/utils/dates'; -import { mockUiSetting } from '../../../../common/mocks/mock_kibana_ui_settings_service'; +import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator'; import { TestProvidersComponent } from '../../../../common/mocks/test_providers'; -import { generateFieldTypeMap } from '../../../../common/mocks/mock_field_type_map'; -import { unwrapValue } from '../../lib/unwrap_value'; const mockIndicator = generateMockIndicator(); -const mockFieldTypesMap = generateFieldTypeMap(); describe('<IndicatorsFlyout />', () => { - it('should render ioc id in title and first_seen in subtitle', () => { - const { getByTestId } = render( + beforeEach(() => { + render( <TestProvidersComponent> - <IndicatorsFlyout - indicator={mockIndicator} - fieldTypesMap={mockFieldTypesMap} - closeFlyout={() => {}} - /> + <IndicatorsFlyout indicator={mockIndicator} closeFlyout={() => {}} /> </TestProvidersComponent> ); - - expect(getByTestId(TITLE_TEST_ID).innerHTML).toContain( - `Indicator: ${unwrapValue(mockIndicator, RawIndicatorFieldId.Name)}` - ); - expect(getByTestId(SUBTITLE_TEST_ID).innerHTML).toContain( - `First seen: ${dateFormatter( - unwrapValue(mockIndicator, RawIndicatorFieldId.FirstSeen) as string, - mockUiSetting('dateFormat:tz') as string, - mockUiSetting('dateFormat') as string - )}` - ); }); - it(`should render ${EMPTY_VALUE} in on invalid indicator first_seen value`, () => { - const { getByTestId } = render( - <TestProvidersComponent> - <IndicatorsFlyout indicator={{ fields: {} }} fieldTypesMap={{}} closeFlyout={() => {}} /> - </TestProvidersComponent> - ); + it('should render all the tab switches', () => { + expect(screen.queryByTestId('tiIndicatorFlyoutTabs')).toBeInTheDocument(); - expect(getByTestId(TITLE_TEST_ID).innerHTML).toContain(`Indicator: ${EMPTY_VALUE}`); - expect(getByTestId(SUBTITLE_TEST_ID).innerHTML).toContain(`First seen: ${EMPTY_VALUE}`); - }); + const switchElement = screen.getByTestId('tiIndicatorFlyoutTabs'); - it(`should render ${EMPTY_VALUE} in title and subtitle on invalid indicator`, () => { - const { getByTestId } = render( - <TestProvidersComponent> - <IndicatorsFlyout - indicator={{ fields: { 'threat.indicator.first_seen': ['abc'] } }} - fieldTypesMap={mockFieldTypesMap} - closeFlyout={() => {}} - /> - </TestProvidersComponent> - ); + expect(switchElement).toHaveTextContent(/Overview/); + expect(switchElement).toHaveTextContent(/Table/); + expect(switchElement).toHaveTextContent(/JSON/); + }); - expect(getByTestId(TITLE_TEST_ID).innerHTML).toContain(`Indicator: ${EMPTY_VALUE}`); - expect(getByTestId(SUBTITLE_TEST_ID).innerHTML).toContain(`First seen: ${EMPTY_VALUE}`); + describe('title and subtitle', () => { + describe('valid indicator', () => { + it('should render correct title and subtitle', async () => { + expect(screen.getByTestId(TITLE_TEST_ID)).toHaveTextContent('Indicator details'); + }); + }); + + describe('invalid indicator', () => { + beforeEach(() => { + cleanup(); + + render( + <TestProvidersComponent> + <IndicatorsFlyout + indicator={{ fields: {} } as unknown as Indicator} + closeFlyout={() => {}} + /> + </TestProvidersComponent> + ); + }); + + it('should render correct labels', () => { + expect(screen.getByTestId(TITLE_TEST_ID)).toHaveTextContent('Indicator details'); + expect(screen.getByTestId(SUBTITLE_TEST_ID)).toHaveTextContent('First seen: -'); + }); + }); }); }); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.tsx index f1dde44d05aabb..4026980ae1c892 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/indicators_flyout.tsx @@ -19,17 +19,18 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { DateFormatter } from '../../../../components/date_formatter/date_formatter'; -import { EMPTY_VALUE } from '../../../../../common/constants'; import { Indicator, RawIndicatorFieldId } from '../../../../../common/types/indicator'; -import { IndicatorsFlyoutJson } from '../indicators_flyout_json/indicators_flyout_json'; -import { IndicatorsFlyoutTable } from '../indicators_flyout_table/indicators_flyout_table'; +import { IndicatorsFlyoutJson } from './tabs/indicators_flyout_json/indicators_flyout_json'; +import { IndicatorsFlyoutTable } from './tabs/indicators_flyout_table/indicators_flyout_table'; import { unwrapValue } from '../../lib/unwrap_value'; +import { IndicatorsFlyoutOverview } from './tabs/indicators_flyout_overview'; export const TITLE_TEST_ID = 'tiIndicatorFlyoutTitle'; export const SUBTITLE_TEST_ID = 'tiIndicatorFlyoutSubtitle'; export const TABS_TEST_ID = 'tiIndicatorFlyoutTabs'; const enum TAB_IDS { + overview, table, json, } @@ -39,10 +40,6 @@ export interface IndicatorsFlyoutProps { * Indicator passed down to the different tabs (table and json views). */ indicator: Indicator; - /** - * Object mapping each field with their type to ease display in the {@link IndicatorsFlyoutTable} component. - */ - fieldTypesMap: { [id: string]: string }; /** * Event to close flyout (used by {@link EuiFlyout}). */ @@ -52,15 +49,26 @@ export interface IndicatorsFlyoutProps { /** * Leverages the {@link EuiFlyout} from the @elastic/eui library to dhow the details of a specific {@link Indicator}. */ -export const IndicatorsFlyout: VFC<IndicatorsFlyoutProps> = ({ - indicator, - fieldTypesMap, - closeFlyout, -}) => { - const [selectedTabId, setSelectedTabId] = useState(TAB_IDS.table); +export const IndicatorsFlyout: VFC<IndicatorsFlyoutProps> = ({ indicator, closeFlyout }) => { + const [selectedTabId, setSelectedTabId] = useState(TAB_IDS.overview); const tabs = useMemo( () => [ + { + id: TAB_IDS.overview, + name: ( + <FormattedMessage + id="xpack.threatIntelligence.indicator.flyout.overviewTabLabel" + defaultMessage="Overview" + /> + ), + content: ( + <IndicatorsFlyoutOverview + indicator={indicator} + onViewAllFieldsInTable={() => setSelectedTabId(TAB_IDS.table)} + /> + ), + }, { id: TAB_IDS.table, name: ( @@ -69,7 +77,7 @@ export const IndicatorsFlyout: VFC<IndicatorsFlyoutProps> = ({ defaultMessage="Table" /> ), - content: <IndicatorsFlyoutTable indicator={indicator} fieldTypesMap={fieldTypesMap} />, + content: <IndicatorsFlyoutTable indicator={indicator} />, }, { id: TAB_IDS.json, @@ -82,7 +90,7 @@ export const IndicatorsFlyout: VFC<IndicatorsFlyoutProps> = ({ content: <IndicatorsFlyoutJson indicator={indicator} />, }, ], - [indicator, fieldTypesMap] + [indicator] ); const onSelectedTabChanged = (id: number) => setSelectedTabId(id); @@ -102,7 +110,7 @@ export const IndicatorsFlyout: VFC<IndicatorsFlyoutProps> = ({ ); const firstSeen: string = unwrapValue(indicator, RawIndicatorFieldId.FirstSeen) as string; - const displayNameValue = unwrapValue(indicator, RawIndicatorFieldId.Name) || EMPTY_VALUE; + const flyoutTitleId = useGeneratedHtmlId({ prefix: 'simpleFlyoutTitle', }); @@ -113,9 +121,8 @@ export const IndicatorsFlyout: VFC<IndicatorsFlyoutProps> = ({ <EuiTitle> <h2 data-test-subj={TITLE_TEST_ID} id={flyoutTitleId}> <FormattedMessage - id="xpack.threatIntelligence.indicator.flyout.panelTitle" - defaultMessage="Indicator: {title}" - values={{ title: displayNameValue }} + id="xpack.threatIntelligence.indicator.flyout.panelTitleWithOverviewTab" + defaultMessage="Indicator details" /> </h2> </EuiTitle> diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_json/index.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_json/index.tsx similarity index 100% rename from x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_json/index.tsx rename to x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_json/index.tsx diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_json/indicators_flyout_json.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_json/indicators_flyout_json.stories.tsx similarity index 96% rename from x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_json/indicators_flyout_json.stories.tsx rename to x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_json/indicators_flyout_json.stories.tsx index 447dcfcdda6eee..1e40c23a26d4de 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_json/indicators_flyout_json.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_json/indicators_flyout_json.stories.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { Story } from '@storybook/react'; -import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator'; +import { generateMockIndicator, Indicator } from '../../../../../../../common/types/indicator'; import { IndicatorsFlyoutJson } from './indicators_flyout_json'; export default { diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_json/indicators_flyout_json.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_json/indicators_flyout_json.test.tsx similarity index 82% rename from x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_json/indicators_flyout_json.test.tsx rename to x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_json/indicators_flyout_json.test.tsx index 3310a4e1c893c2..a468db60a023e7 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_json/indicators_flyout_json.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_json/indicators_flyout_json.test.tsx @@ -7,16 +7,13 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { TestProvidersComponent } from '../../../../common/mocks/test_providers'; -import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator'; -import { - CODE_BLOCK_TEST_ID, - EMPTY_PROMPT_TEST_ID, - IndicatorsFlyoutJson, -} from './indicators_flyout_json'; +import { TestProvidersComponent } from '../../../../../../common/mocks/test_providers'; +import { generateMockIndicator, Indicator } from '../../../../../../../common/types/indicator'; +import { CODE_BLOCK_TEST_ID, IndicatorsFlyoutJson } from './indicators_flyout_json'; const mockIndicator: Indicator = generateMockIndicator(); +import { EMPTY_PROMPT_TEST_ID } from '../../components/indicator_empty_prompt'; describe('<IndicatorsFlyoutJson />', () => { it('should render code block component on valid indicator', () => { const { getByTestId } = render( diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_json/indicators_flyout_json.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_json/indicators_flyout_json.tsx similarity index 52% rename from x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_json/indicators_flyout_json.tsx rename to x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_json/indicators_flyout_json.tsx index 6bc42b1980c201..99c4bcfb0d50f7 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_json/indicators_flyout_json.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_json/indicators_flyout_json.tsx @@ -6,11 +6,10 @@ */ import React, { VFC } from 'react'; -import { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { Indicator } from '../../../../../common/types/indicator'; +import { EuiCodeBlock } from '@elastic/eui'; +import { Indicator } from '../../../../../../../common/types/indicator'; +import { IndicatorEmptyPrompt } from '../../components/indicator_empty_prompt'; -export const EMPTY_PROMPT_TEST_ID = 'tiFlyoutJsonEmptyPrompt'; export const CODE_BLOCK_TEST_ID = 'tiFlyoutJsonCodeBlock'; export interface IndicatorsFlyoutJsonProps { @@ -26,27 +25,7 @@ export interface IndicatorsFlyoutJsonProps { */ export const IndicatorsFlyoutJson: VFC<IndicatorsFlyoutJsonProps> = ({ indicator }) => { return Object.keys(indicator).length === 0 ? ( - <EuiEmptyPrompt - iconType="alert" - color="danger" - title={ - <h2> - <FormattedMessage - id="xpack.threatIntelligence.indicator.flyoutJson.errorMessageTitle" - defaultMessage="Unable to display indicator information" - /> - </h2> - } - body={ - <p> - <FormattedMessage - id="xpack.threatIntelligence.indicator.flyoutJson.errorMessageBody" - defaultMessage="There was an error displaying the indicator fields and values." - /> - </p> - } - data-test-subj={EMPTY_PROMPT_TEST_ID} - /> + <IndicatorEmptyPrompt /> ) : ( <EuiCodeBlock language="json" lineNumbers data-test-subj={CODE_BLOCK_TEST_ID}> {JSON.stringify(indicator, null, 2)} diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/index.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/components/block/index.tsx similarity index 87% rename from x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/index.tsx rename to x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/components/block/index.tsx index acea79493c0be8..dbad4c02ee5dce 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicator_field/index.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/components/block/index.tsx @@ -5,4 +5,4 @@ * 2.0. */ -export * from './indicator_field'; +export * from './indicator_block'; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/components/block/indicator_block.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/components/block/indicator_block.stories.tsx new file mode 100644 index 00000000000000..32966e72c2eec2 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/components/block/indicator_block.stories.tsx @@ -0,0 +1,31 @@ +/* + * 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 from 'react'; +import { IndicatorsFiltersContext } from '../../../../../../context'; +import { StoryProvidersComponent } from '../../../../../../../../common/mocks/story_providers'; +import { generateMockIndicator } from '../../../../../../../../../common/types/indicator'; +import { IndicatorBlock } from './indicator_block'; + +export default { + component: IndicatorBlock, + title: 'IndicatorBlock', +}; + +const mockIndicator = generateMockIndicator(); + +export function Default() { + const mockField = 'threat.indicator.ip'; + + return ( + <StoryProvidersComponent> + <IndicatorsFiltersContext.Provider value={{} as any}> + <IndicatorBlock indicator={mockIndicator} field={mockField} /> + </IndicatorsFiltersContext.Provider> + </StoryProvidersComponent> + ); +} diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/components/block/indicator_block.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/components/block/indicator_block.tsx new file mode 100644 index 00000000000000..3fbd7d7365d506 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/components/block/indicator_block.tsx @@ -0,0 +1,70 @@ +/* + * 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 { EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; +import React, { VFC } from 'react'; +import { euiStyled, css } from '@kbn/kibana-react-plugin/common'; +import { Indicator } from '../../../../../../../../../common/types/indicator'; +import { IndicatorFieldValue } from '../../../../../indicator_field_value'; +import { IndicatorFieldLabel } from '../../../../../indicator_field_label'; +import { IndicatorValueActions } from '../../../../../indicator_value_actions'; + +/** + * Show actions wrapper on hover. This is a helper component, limited only to Block + */ +const VisibleOnHover = euiStyled.div` + ${({ theme }) => css` + & { + height: 100%; + } + + & .actionsWrapper { + visibility: hidden; + display: inline-block; + margin-inline-start: ${theme.eui.euiSizeXS}; + } + + &:hover .actionsWrapper { + visibility: visible; + } + `} +`; + +const panelProps = { + color: 'subdued' as const, + hasShadow: false, + borderRadius: 'none' as const, + paddingSize: 's' as const, +}; + +export interface IndicatorBlockProps { + indicator: Indicator; + field: string; + ['data-test-subj']?: string; +} + +/** + * Renders indicator field value in a rectangle, to highlight it even more + */ +export const IndicatorBlock: VFC<IndicatorBlockProps> = ({ field, indicator, ...props }) => { + return ( + <EuiPanel {...panelProps}> + <VisibleOnHover data-test-subj={`${props['data-test-subj']}Item`}> + <EuiText> + <IndicatorFieldLabel field={field} /> + </EuiText> + <EuiSpacer size="s" /> + <EuiText size="s"> + <IndicatorFieldValue indicator={indicator} field={field} /> + <span className="actionsWrapper"> + <IndicatorValueActions indicator={indicator} field={field} {...props} /> + </span> + </EuiText> + </VisibleOnHover> + </EuiPanel> + ); +}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/components/highlighted_values_table/highlighted_values_table.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/components/highlighted_values_table/highlighted_values_table.tsx new file mode 100644 index 00000000000000..d7cf25dca3239b --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/components/highlighted_values_table/highlighted_values_table.tsx @@ -0,0 +1,54 @@ +/* + * 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, { useMemo, VFC } from 'react'; +import { Indicator, RawIndicatorFieldId } from '../../../../../../../../../common/types/indicator'; +import { unwrapValue } from '../../../../../../lib/unwrap_value'; +import { IndicatorFieldsTable } from '../../../../components/indicator_fields_table'; + +/** + * Pick indicator fields starting with the indicator type + */ +const byIndicatorType = (indicatorType: string) => (field: string) => + field.startsWith(`threat.indicator.${indicatorType}`) || + [ + 'threat.indicator.reference', + 'threat.indicator.description', + 'threat.software.alias', + 'threat.indicator.confidence', + 'threat.tactic.name', + 'threat.tactic.reference', + ].includes(field); + +interface HighlightedValuesTableProps { + indicator: Indicator; + ['data-test-subj']?: string; +} + +/** + * Displays highlighted indicator values based on indicator type + */ +export const HighlightedValuesTable: VFC<HighlightedValuesTableProps> = ({ + indicator, + ...props +}) => { + const indicatorType = unwrapValue(indicator, RawIndicatorFieldId.Type); + + const highlightedFields: string[] = useMemo( + () => Object.keys(indicator.fields).filter(byIndicatorType(indicatorType || '')), + [indicator.fields, indicatorType] + ); + + return ( + <IndicatorFieldsTable + search={false} + indicator={indicator} + fields={highlightedFields} + {...props} + /> + ); +}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/components/highlighted_values_table/index.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/components/highlighted_values_table/index.tsx new file mode 100644 index 00000000000000..d31d18ec79367b --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/components/highlighted_values_table/index.tsx @@ -0,0 +1,8 @@ +/* + * 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 * from './highlighted_values_table'; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/index.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/index.tsx new file mode 100644 index 00000000000000..71fcb871adf42e --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/index.tsx @@ -0,0 +1,8 @@ +/* + * 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 * from './indicators_flyout_overview'; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/indicators_flyout_overview.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/indicators_flyout_overview.stories.tsx new file mode 100644 index 00000000000000..72b20f769575b7 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/indicators_flyout_overview.stories.tsx @@ -0,0 +1,36 @@ +/* + * 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 from 'react'; +import { Story } from '@storybook/react'; +import { StoryProvidersComponent } from '../../../../../../common/mocks/story_providers'; +import { generateMockIndicator, Indicator } from '../../../../../../../common/types/indicator'; +import { IndicatorsFlyoutOverview } from './indicators_flyout_overview'; +import { IndicatorsFiltersContext } from '../../../../context'; + +export default { + component: IndicatorsFlyoutOverview, + title: 'IndicatorsFlyoutOverview', + parameters: { + backgrounds: { + default: 'white', + values: [{ name: 'white', value: '#fff' }], + }, + }, +}; + +export const Default: Story<void> = () => { + const mockIndicator: Indicator = generateMockIndicator(); + + return ( + <StoryProvidersComponent> + <IndicatorsFiltersContext.Provider value={{} as any}> + <IndicatorsFlyoutOverview onViewAllFieldsInTable={() => {}} indicator={mockIndicator} /> + </IndicatorsFiltersContext.Provider> + </StoryProvidersComponent> + ); +}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/indicators_flyout_overview.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/indicators_flyout_overview.test.tsx new file mode 100644 index 00000000000000..580534e5668c20 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/indicators_flyout_overview.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TestProvidersComponent } from '../../../../../../common/mocks/test_providers'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { generateMockIndicator, Indicator } from '../../../../../../../common/types/indicator'; +import { + IndicatorsFlyoutOverview, + TI_FLYOUT_OVERVIEW_HIGH_LEVEL_BLOCKS, + TI_FLYOUT_OVERVIEW_TABLE, +} from './indicators_flyout_overview'; +import { EMPTY_PROMPT_TEST_ID } from '../../components/indicator_empty_prompt'; + +describe('<IndicatorsFlyoutOverview />', () => { + describe('invalid indicator', () => { + it('should render error message on invalid indicator', () => { + render( + <TestProvidersComponent> + <IndicatorsFlyoutOverview + onViewAllFieldsInTable={() => {}} + indicator={{ fields: {} } as unknown as Indicator} + /> + </TestProvidersComponent> + ); + + expect(screen.getByTestId(EMPTY_PROMPT_TEST_ID)).toBeInTheDocument(); + }); + }); + + it('should render the highlighted blocks and table when valid indicator is passed', () => { + render( + <TestProvidersComponent> + <IndicatorsFlyoutOverview + onViewAllFieldsInTable={() => {}} + indicator={generateMockIndicator()} + /> + </TestProvidersComponent> + ); + + expect(screen.queryByTestId(TI_FLYOUT_OVERVIEW_TABLE)).toBeInTheDocument(); + expect(screen.queryByTestId(TI_FLYOUT_OVERVIEW_HIGH_LEVEL_BLOCKS)).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/indicators_flyout_overview.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/indicators_flyout_overview.tsx new file mode 100644 index 00000000000000..c1a0468822269e --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_overview/indicators_flyout_overview.tsx @@ -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 { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useMemo } from 'react'; +import { VFC } from 'react'; +import { EMPTY_VALUE } from '../../../../../../../common/constants'; +import { Indicator, RawIndicatorFieldId } from '../../../../../../../common/types/indicator'; +import { unwrapValue } from '../../../../lib/unwrap_value'; +import { IndicatorEmptyPrompt } from '../../components/indicator_empty_prompt'; +import { IndicatorBlock } from './components/block'; +import { HighlightedValuesTable } from './components/highlighted_values_table'; + +const highLevelFields = [ + RawIndicatorFieldId.Feed, + RawIndicatorFieldId.Type, + RawIndicatorFieldId.MarkingTLP, + RawIndicatorFieldId.Confidence, +]; + +export const TI_FLYOUT_OVERVIEW_TABLE = 'tiFlyoutOverviewTableRow'; +export const TI_FLYOUT_OVERVIEW_HIGH_LEVEL_BLOCKS = 'tiFlyoutOverviewHighLevelBlocks'; + +export interface IndicatorsFlyoutOverviewProps { + indicator: Indicator; + onViewAllFieldsInTable: VoidFunction; +} + +export const IndicatorsFlyoutOverview: VFC<IndicatorsFlyoutOverviewProps> = ({ + indicator, + onViewAllFieldsInTable, +}) => { + const indicatorType = unwrapValue(indicator, RawIndicatorFieldId.Type); + + const highLevelBlocks = useMemo( + () => ( + <EuiFlexGroup data-test-subj={TI_FLYOUT_OVERVIEW_HIGH_LEVEL_BLOCKS}> + {highLevelFields.map((field) => ( + <EuiFlexItem key={field}> + <IndicatorBlock + indicator={indicator} + field={field} + data-test-subj={TI_FLYOUT_OVERVIEW_HIGH_LEVEL_BLOCKS} + /> + </EuiFlexItem> + ))} + </EuiFlexGroup> + ), + [indicator] + ); + + const indicatorDescription = useMemo(() => { + const unwrappedDescription = unwrapValue(indicator, RawIndicatorFieldId.Description); + + return unwrappedDescription ? <EuiText>{unwrappedDescription}</EuiText> : null; + }, [indicator]); + + const indicatorName = unwrapValue(indicator, RawIndicatorFieldId.Name) || EMPTY_VALUE; + + if (!indicatorType) { + return <IndicatorEmptyPrompt />; + } + + return ( + <> + <EuiTitle> + <h2>{indicatorName}</h2> + </EuiTitle> + + {indicatorDescription} + + <EuiSpacer /> + + {highLevelBlocks} + + <EuiHorizontalRule /> + + <EuiFlexGroup> + <EuiFlexItem> + <EuiTitle size="xxs"> + <h5> + <FormattedMessage + id="xpack.threatIntelligence.indicator.flyoutOverviewTable.highlightedFields" + defaultMessage="Highlighted fields" + /> + </h5> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonEmpty onClick={onViewAllFieldsInTable}> + <FormattedMessage + id="xpack.threatIntelligence.indicator.flyoutOverviewTable.viewAllFieldsInTable" + defaultMessage="View all fields in table" + /> + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + + <HighlightedValuesTable indicator={indicator} data-test-subj={TI_FLYOUT_OVERVIEW_TABLE} /> + </> + ); +}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_table/index.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_table/index.tsx similarity index 100% rename from x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_table/index.tsx rename to x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_table/index.tsx diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_table/indicators_flyout_table.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_table/indicators_flyout_table.stories.tsx similarity index 62% rename from x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_table/indicators_flyout_table.stories.tsx rename to x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_table/indicators_flyout_table.stories.tsx index e864d292c61cb1..3a3aa1fa788ee1 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_table/indicators_flyout_table.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_table/indicators_flyout_table.stories.tsx @@ -9,13 +9,12 @@ import React from 'react'; import { Story } from '@storybook/react'; import { CoreStart } from '@kbn/core/public'; import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; -import { mockIndicatorsFiltersContext } from '../../../../common/mocks/mock_indicators_filters_context'; -import { generateFieldTypeMap } from '../../../../common/mocks/mock_field_type_map'; -import { mockUiSettingsService } from '../../../../common/mocks/mock_kibana_ui_settings_service'; -import { mockKibanaTimelinesService } from '../../../../common/mocks/mock_kibana_timelines_service'; -import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator'; +import { mockIndicatorsFiltersContext } from '../../../../../../common/mocks/mock_indicators_filters_context'; +import { mockUiSettingsService } from '../../../../../../common/mocks/mock_kibana_ui_settings_service'; +import { mockKibanaTimelinesService } from '../../../../../../common/mocks/mock_kibana_timelines_service'; +import { generateMockIndicator, Indicator } from '../../../../../../../common/types/indicator'; import { IndicatorsFlyoutTable } from './indicators_flyout_table'; -import { IndicatorsFiltersContext } from '../../context'; +import { IndicatorsFiltersContext } from '../../../../context'; export default { component: IndicatorsFlyoutTable, @@ -24,7 +23,6 @@ export default { export const Default: Story<void> = () => { const mockIndicator: Indicator = generateMockIndicator(); - const mockFieldTypesMap = generateFieldTypeMap(); const KibanaReactContext = createKibanaReactContext({ uiSettings: mockUiSettingsService(), @@ -34,14 +32,12 @@ export const Default: Story<void> = () => { return ( <KibanaReactContext.Provider> <IndicatorsFiltersContext.Provider value={mockIndicatorsFiltersContext}> - <IndicatorsFlyoutTable indicator={mockIndicator} fieldTypesMap={mockFieldTypesMap} /> + <IndicatorsFlyoutTable indicator={mockIndicator} /> </IndicatorsFiltersContext.Provider> </KibanaReactContext.Provider> ); }; export const EmptyIndicator: Story<void> = () => { - return ( - <IndicatorsFlyoutTable indicator={{ fields: {} } as unknown as Indicator} fieldTypesMap={{}} /> - ); + return <IndicatorsFlyoutTable indicator={{ fields: {} } as unknown as Indicator} />; }; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_table/indicators_flyout_table.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_table/indicators_flyout_table.test.tsx similarity index 72% rename from x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_table/indicators_flyout_table.test.tsx rename to x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_table/indicators_flyout_table.test.tsx index bc7cfffbcf7a31..c91ce0e6aa89c8 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_table/indicators_flyout_table.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_table/indicators_flyout_table.test.tsx @@ -7,28 +7,23 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { TestProvidersComponent } from '../../../../common/mocks/test_providers'; +import { TestProvidersComponent } from '../../../../../../common/mocks/test_providers'; import { generateMockIndicator, Indicator, RawIndicatorFieldId, -} from '../../../../../common/types/indicator'; -import { generateFieldTypeMap } from '../../../../common/mocks/mock_field_type_map'; -import { - EMPTY_PROMPT_TEST_ID, - IndicatorsFlyoutTable, - TABLE_TEST_ID, -} from './indicators_flyout_table'; -import { unwrapValue } from '../../lib/unwrap_value'; +} from '../../../../../../../common/types/indicator'; +import { IndicatorsFlyoutTable, TABLE_TEST_ID } from './indicators_flyout_table'; +import { unwrapValue } from '../../../../lib/unwrap_value'; +import { EMPTY_PROMPT_TEST_ID } from '../../components/indicator_empty_prompt'; const mockIndicator: Indicator = generateMockIndicator(); -const mockFieldTypesMap = generateFieldTypeMap(); describe('<IndicatorsFlyoutTable />', () => { it('should render fields and values in table', () => { const { getByTestId, getByText, getAllByText } = render( <TestProvidersComponent> - <IndicatorsFlyoutTable indicator={mockIndicator} fieldTypesMap={mockFieldTypesMap} /> + <IndicatorsFlyoutTable indicator={mockIndicator} /> </TestProvidersComponent> ); @@ -49,7 +44,7 @@ describe('<IndicatorsFlyoutTable />', () => { it('should render error message on invalid indicator', () => { const { getByTestId, getByText } = render( <TestProvidersComponent> - <IndicatorsFlyoutTable indicator={{ fields: {} }} fieldTypesMap={{}} /> + <IndicatorsFlyoutTable indicator={{ fields: {} }} /> </TestProvidersComponent> ); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_table/indicators_flyout_table.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_table/indicators_flyout_table.tsx new file mode 100644 index 00000000000000..8bb7956afc1701 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout/tabs/indicators_flyout_table/indicators_flyout_table.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { VFC } from 'react'; +import { Indicator } from '../../../../../../../common/types/indicator'; +import { IndicatorEmptyPrompt } from '../../components/indicator_empty_prompt'; +import { IndicatorFieldsTable } from '../../components/indicator_fields_table'; + +export const TABLE_TEST_ID = 'tiFlyoutTableTabRow'; + +const search = { + box: { + incremental: true, + schema: true, + }, +}; + +export interface IndicatorsFlyoutTableProps { + /** + * Indicator to display in table view. + */ + indicator: Indicator; +} + +/** + * Displays all the properties and values of an {@link Indicator} in table view, + * using the {@link EuiInMemoryTable} from the @elastic/eui library. + */ +export const IndicatorsFlyoutTable: VFC<IndicatorsFlyoutTableProps> = ({ indicator }) => { + const items: string[] = Object.keys(indicator.fields); + + return items.length === 0 ? ( + <IndicatorEmptyPrompt /> + ) : ( + <IndicatorFieldsTable + data-test-subj={TABLE_TEST_ID} + search={search} + fields={items} + indicator={indicator} + /> + ); +}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_table/indicators_flyout_table.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_table/indicators_flyout_table.tsx deleted file mode 100644 index c6a64c51629afb..00000000000000 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_flyout_table/indicators_flyout_table.tsx +++ /dev/null @@ -1,120 +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 { EuiEmptyPrompt, EuiInMemoryTable } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React, { VFC } from 'react'; -import { FilterInOut } from '../../../query_bar/components/filter_in_out'; -import { Indicator } from '../../../../../common/types/indicator'; -import { AddToTimeline } from '../../../timeline/components/add_to_timeline'; -import { IndicatorField } from '../indicator_field/indicator_field'; - -export const EMPTY_PROMPT_TEST_ID = 'tiFlyoutTableEmptyPrompt'; -export const TABLE_TEST_ID = 'tiFlyoutTableMemoryTable'; -export const TIMELINE_BUTTON_TEST_ID = 'tiFlyoutTableRowTimelineButton'; - -const search = { - box: { - incremental: true, - schema: true, - }, -}; - -export interface IndicatorsFlyoutTableProps { - /** - * Indicator to display in table view. - */ - indicator: Indicator; - /** - * Object mapping each field with their type to ease display in the {@link IndicatorField} component. - */ - fieldTypesMap: { [id: string]: string }; -} - -/** - * Displays all the properties and values of an {@link Indicator} in table view, - * using the {@link EuiInMemoryTable} from the @elastic/eui library. - */ -export const IndicatorsFlyoutTable: VFC<IndicatorsFlyoutTableProps> = ({ - indicator, - fieldTypesMap, -}) => { - const items: string[] = Object.keys(indicator.fields); - const columns = [ - { - name: ( - <FormattedMessage - id="xpack.threatIntelligence.indicator.flyoutTable.actionsColumnLabel" - defaultMessage="Actions" - /> - ), - actions: [ - { - render: (field: string) => ( - <> - <FilterInOut data={indicator} field={field} /> - <AddToTimeline data={indicator} field={field} testId={TIMELINE_BUTTON_TEST_ID} /> - </> - ), - }, - ], - width: '72px', - }, - { - name: ( - <FormattedMessage - id="xpack.threatIntelligence.indicator.flyoutTable.fieldColumnLabel" - defaultMessage="Field" - /> - ), - render: (field: string) => field, - }, - { - name: ( - <FormattedMessage - id="xpack.threatIntelligence.indicator.flyoutTable.valueColumnLabel" - defaultMessage="Value" - /> - ), - render: (field: string) => ( - <IndicatorField indicator={indicator} field={field} fieldTypesMap={fieldTypesMap} /> - ), - }, - ]; - - return items.length === 0 ? ( - <EuiEmptyPrompt - iconType="alert" - color="danger" - title={ - <h2> - <FormattedMessage - id="xpack.threatIntelligence.indicator.flyoutTable.errorMessageTitle" - defaultMessage="Unable to display indicator information" - /> - </h2> - } - body={ - <p> - <FormattedMessage - id="xpack.threatIntelligence.indicator.flyoutTable.errorMessageBody" - defaultMessage="There was an error displaying the indicator fields and values." - /> - </p> - } - data-test-subj={EMPTY_PROMPT_TEST_ID} - /> - ) : ( - <EuiInMemoryTable - items={items} - columns={columns} - search={search} - sorting={true} - data-test-subj={TABLE_TEST_ID} - /> - ); -}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/cell_actions.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/cell_actions.tsx index db3de2eeb67e77..fa255f053ab673 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/cell_actions.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/cell_actions.tsx @@ -7,14 +7,18 @@ import React, { VFC } from 'react'; import { EuiDataGridColumnCellActionProps } from '@elastic/eui/src/components/datagrid/data_grid_types'; -import { FilterInOut } from '../../../query_bar/components/filter_in_out'; +import { ComponentType } from '../../../../../common/types/component_type'; import { EMPTY_VALUE } from '../../../../../common/constants'; import { Indicator } from '../../../../../common/types/indicator'; import { Pagination } from '../../hooks/use_indicators'; import { AddToTimeline } from '../../../timeline/components/add_to_timeline'; import { getIndicatorFieldAndValue } from '../../lib/field_value'; +import { FilterIn } from '../../../query_bar/components/filter_in'; +import { FilterOut } from '../../../query_bar/components/filter_out'; export const CELL_TIMELINE_BUTTON_TEST_ID = 'tiIndicatorsTableCellTimelineButton'; +export const CELL_FILTER_IN_BUTTON_TEST_ID = 'tiIndicatorsTableCellFilterInButton'; +export const CELL_FILTER_OUT_BUTTON_TEST_ID = 'tiIndicatorsTableCellFilterOutButton'; export interface CellActionsProps extends Omit<EuiDataGridColumnCellActionProps, 'colIndex' | 'isExpanded'> { @@ -50,12 +54,25 @@ export const CellActions: VFC<CellActionsProps> = ({ return ( <> - <FilterInOut Component={Component} data={indicator} field={key} /> + <FilterIn + as={Component} + data={indicator} + field={key} + type={ComponentType.EuiDataGrid} + data-test-subj={CELL_FILTER_IN_BUTTON_TEST_ID} + /> + <FilterOut + as={Component} + data={indicator} + field={key} + type={ComponentType.EuiDataGrid} + data-test-subj={CELL_FILTER_OUT_BUTTON_TEST_ID} + /> <AddToTimeline data={indicator} field={key} - component={Component} - testId={CELL_TIMELINE_BUTTON_TEST_ID} + as={Component} + data-test-subj={CELL_TIMELINE_BUTTON_TEST_ID} /> </> ); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/cell_renderer.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/cell_renderer.tsx index e0b4e1e88daa04..b95a378a35a5b2 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/cell_renderer.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/cell_renderer.tsx @@ -11,7 +11,7 @@ import { euiLightVars as themeLight, euiDarkVars as themeDark } from '@kbn/ui-th import React from 'react'; import { useKibana } from '../../../../hooks/use_kibana'; import { Indicator } from '../../../../../common/types/indicator'; -import { IndicatorField } from '../indicator_field/indicator_field'; +import { IndicatorFieldValue } from '../indicator_field_value/indicator_field_value'; import { IndicatorsTableContext } from './context'; import { ActionsRowCell } from './actions_row_cell'; @@ -29,7 +29,7 @@ export const cellRendererFactory = (from: number) => { const darkMode = uiSettings.get('theme:darkMode'); - const { indicators, expanded, fieldTypesMap } = indicatorsTableContext; + const { indicators, expanded } = indicatorsTableContext; const indicator: Indicator | undefined = indicators[rowIndex - from]; @@ -53,6 +53,6 @@ export const cellRendererFactory = (from: number) => { return <ActionsRowCell indicator={indicator} />; } - return <IndicatorField indicator={indicator} field={columnId} fieldTypesMap={fieldTypesMap} />; + return <IndicatorFieldValue indicator={indicator} field={columnId} />; }; }; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/context.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/context.ts index bd8c619edfd040..41596a257dc9e6 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/context.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/context.ts @@ -12,7 +12,6 @@ export interface IndicatorsTableContextValue { expanded: Indicator | undefined; setExpanded: Dispatch<SetStateAction<Indicator | undefined>>; indicators: Indicator[]; - fieldTypesMap: { [id: string]: string }; } export const IndicatorsTableContext = createContext<IndicatorsTableContextValue | undefined>( diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/use_column_settings.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/use_column_settings.ts index 266ef8811955d0..8315f41725ed0c 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/use_column_settings.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/use_column_settings.ts @@ -7,52 +7,22 @@ import { EuiDataGridColumn } from '@elastic/eui'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { i18n } from '@kbn/i18n'; import negate from 'lodash/negate'; import { RawIndicatorFieldId } from '../../../../../../common/types/indicator'; import { useKibana } from '../../../../../hooks/use_kibana'; +import { translateFieldLabel } from '../../indicator_field_label'; const DEFAULT_COLUMNS: EuiDataGridColumn[] = [ - { - id: RawIndicatorFieldId.TimeStamp, - displayAsText: i18n.translate('xpack.threatIntelligence.indicator.table.timestampColumnTitle', { - defaultMessage: '@timestamp', - }), - }, - { - id: RawIndicatorFieldId.Name, - displayAsText: i18n.translate('xpack.threatIntelligence.indicator.table.indicatorColumTitle', { - defaultMessage: 'Indicator', - }), - }, - { - id: RawIndicatorFieldId.Type, - displayAsText: i18n.translate( - 'xpack.threatIntelligence.indicator.table.indicatorTypeColumTitle', - { - defaultMessage: 'Indicator type', - } - ), - }, - { - id: RawIndicatorFieldId.Feed, - displayAsText: i18n.translate('xpack.threatIntelligence.indicator.table.FeedColumTitle', { - defaultMessage: 'Feed', - }), - }, - { - id: RawIndicatorFieldId.FirstSeen, - displayAsText: i18n.translate('xpack.threatIntelligence.indicator.table.firstSeenColumTitle', { - defaultMessage: 'First seen', - }), - }, - { - id: RawIndicatorFieldId.LastSeen, - displayAsText: i18n.translate('xpack.threatIntelligence.indicator.table.lastSeenColumTitle', { - defaultMessage: 'Last seen', - }), - }, -]; + RawIndicatorFieldId.TimeStamp, + RawIndicatorFieldId.Name, + RawIndicatorFieldId.Type, + RawIndicatorFieldId.Feed, + RawIndicatorFieldId.FirstSeen, + RawIndicatorFieldId.LastSeen, +].map((field) => ({ + id: field, + displayAsText: translateFieldLabel(field), +})); const DEFAULT_VISIBLE_COLUMNS = DEFAULT_COLUMNS.map((column) => column.id); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.stories.tsx index e92df3f388e97b..07765dbf601d4d 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.stories.tsx @@ -5,14 +5,10 @@ * 2.0. */ -import { CoreStart } from '@kbn/core/public'; -import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; import React from 'react'; import { DataView } from '@kbn/data-views-plugin/common'; import { mockIndicatorsFiltersContext } from '../../../../common/mocks/mock_indicators_filters_context'; -import { mockTriggersActionsUiService } from '../../../../common/mocks/mock_kibana_triggers_actions_ui_service'; -import { mockUiSettingsService } from '../../../../common/mocks/mock_kibana_ui_settings_service'; -import { mockKibanaTimelinesService } from '../../../../common/mocks/mock_kibana_timelines_service'; +import { StoryProvidersComponent } from '../../../../common/mocks/story_providers'; import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator'; import { IndicatorsTable } from './indicators_table'; import { IndicatorsFiltersContext } from '../../context'; @@ -29,14 +25,8 @@ const stub = () => void 0; export function WithIndicators() { const indicatorsFixture: Indicator[] = Array(10).fill(generateMockIndicator()); - const KibanaReactContext = createKibanaReactContext({ - uiSettings: mockUiSettingsService(), - timelines: mockKibanaTimelinesService, - triggersActionsUi: mockTriggersActionsUiService, - } as unknown as CoreStart); - return ( - <KibanaReactContext.Provider> + <StoryProvidersComponent> <IndicatorsFiltersContext.Provider value={mockIndicatorsFiltersContext}> <IndicatorsTable browserFields={{}} @@ -53,25 +43,27 @@ export function WithIndicators() { indexPattern={mockIndexPattern} /> </IndicatorsFiltersContext.Provider> - </KibanaReactContext.Provider> + </StoryProvidersComponent> ); } export function WithNoIndicators() { return ( - <IndicatorsTable - browserFields={{}} - pagination={{ - pageSize: 10, - pageIndex: 0, - pageSizeOptions: [10, 25, 50], - }} - indicators={[]} - onChangePage={stub} - onChangeItemsPerPage={stub} - indicatorCount={0} - loading={false} - indexPattern={mockIndexPattern} - /> + <StoryProvidersComponent> + <IndicatorsTable + browserFields={{}} + pagination={{ + pageSize: 10, + pageIndex: 0, + pageSizeOptions: [10, 25, 50], + }} + indicators={[]} + onChangePage={stub} + onChangeItemsPerPage={stub} + indicatorCount={0} + loading={false} + indexPattern={mockIndexPattern} + /> + </StoryProvidersComponent> ); } diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.tsx index eea9acaabda31e..a3604b31cf75d4 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.tsx @@ -55,7 +55,6 @@ export const IndicatorsTable: VFC<IndicatorsTableProps> = ({ onChangeItemsPerPage, pagination, loading, - indexPattern, browserFields, }) => { const [expanded, setExpanded] = useState<Indicator>(); @@ -65,35 +64,14 @@ export const IndicatorsTable: VFC<IndicatorsTableProps> = ({ [pagination.pageIndex, pagination.pageSize] ); - // field name to field type map to allow the cell_renderer to format dates - const fieldTypesMap: { [id: string]: string } = useMemo(() => { - if (!indexPattern) return {}; - - const res: { [id: string]: string } = {}; - indexPattern.fields.map((field) => (res[field.name] = field.type)); - return res; - }, [indexPattern]); - const indicatorTableContextValue = useMemo<IndicatorsTableContextValue>( - () => ({ expanded, setExpanded, indicators, fieldTypesMap }), - [expanded, indicators, fieldTypesMap] + () => ({ expanded, setExpanded, indicators }), + [expanded, indicators] ); const start = pagination.pageIndex * pagination.pageSize; const end = start + pagination.pageSize; - const flyoutFragment = useMemo( - () => - expanded ? ( - <IndicatorsFlyout - indicator={expanded} - fieldTypesMap={fieldTypesMap} - closeFlyout={() => setExpanded(undefined)} - /> - ) : null, - [expanded, fieldTypesMap] - ); - const leadingControlColumns = useMemo( () => [ { @@ -140,42 +118,67 @@ export const IndicatorsTable: VFC<IndicatorsTableProps> = ({ onToggleColumn: handleToggleColumn, }); - if (loading) { + const flyoutFragment = useMemo( + () => + expanded ? ( + <IndicatorsFlyout indicator={expanded} closeFlyout={() => setExpanded(undefined)} /> + ) : null, + [expanded] + ); + + const gridFragment = useMemo(() => { + if (loading) { + return ( + <EuiFlexGroup justifyContent="spaceAround"> + <EuiFlexItem grow={false}> + <EuiPanel hasShadow={false} hasBorder={false} paddingSize="xl"> + <EuiLoadingSpinner size="xl" /> + </EuiPanel> + </EuiFlexItem> + </EuiFlexGroup> + ); + } + + if (!indicatorCount) { + return <EmptyState />; + } + return ( - <EuiFlexGroup justifyContent="spaceAround"> - <EuiFlexItem grow={false}> - <EuiPanel hasShadow={false} hasBorder={false} paddingSize="xl"> - <EuiLoadingSpinner size="xl" /> - </EuiPanel> - </EuiFlexItem> - </EuiFlexGroup> + <EuiDataGrid + aria-labelledby="indicators-table" + leadingControlColumns={leadingControlColumns} + columns={columns} + columnVisibility={columnVisibility} + rowCount={indicatorCount} + renderCellValue={renderCellValue} + toolbarVisibility={toolbarOptions} + pagination={{ + ...pagination, + onChangeItemsPerPage, + onChangePage, + }} + gridStyle={gridStyle} + data-test-subj={TABLE_TEST_ID} + /> ); - } - - if (!indicatorCount) { - return <EmptyState />; - } + }, [ + columnVisibility, + columns, + indicatorCount, + leadingControlColumns, + loading, + onChangeItemsPerPage, + onChangePage, + pagination, + renderCellValue, + toolbarOptions, + ]); return ( <div> <IndicatorsTableContext.Provider value={indicatorTableContextValue}> - <EuiDataGrid - aria-labelledby="indicators-table" - leadingControlColumns={leadingControlColumns} - columns={columns} - columnVisibility={columnVisibility} - rowCount={indicatorCount} - renderCellValue={renderCellValue} - toolbarVisibility={toolbarOptions} - pagination={{ - ...pagination, - onChangeItemsPerPage, - onChangePage, - }} - gridStyle={gridStyle} - data-test-subj={TABLE_TEST_ID} - /> {flyoutFragment} + {gridFragment} </IndicatorsTableContext.Provider> </div> ); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts index 9ea7e492c7893d..82e59dc7b3ad33 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts @@ -197,12 +197,12 @@ export const useAggregatedIndicators = ({ ) .subscribe({ next: (response) => { - const aggregations: Aggregation[] = - response.rawResponse.aggregations[AGGREGATION_NAME]?.buckets; - const chartSeries: ChartSeries[] = convertAggregationToChartSeries(aggregations); - setIndicators(chartSeries); - if (isCompleteResponse(response)) { + const aggregations: Aggregation[] = + response.rawResponse.aggregations[AGGREGATION_NAME]?.buckets; + const chartSeries: ChartSeries[] = convertAggregationToChartSeries(aggregations); + setIndicators(chartSeries); + searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { searchSubscription$.current.unsubscribe(); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.tsx index f492ac74dcaa37..c3b643df2ec456 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.tsx @@ -15,6 +15,7 @@ import { useFilters } from '../query_bar/hooks/use_filters'; import { FiltersGlobal } from '../../containers/filters_global'; import QueryBar from '../query_bar/components/query_bar'; import { useSourcererDataView } from './hooks/use_sourcerer_data_view'; +import { FieldTypesProvider } from '../../containers/field_types_provider'; export const IndicatorsPage: VFC = () => { const { browserFields, indexPattern } = useSourcererDataView(); @@ -37,33 +38,35 @@ export const IndicatorsPage: VFC = () => { }); return ( - <DefaultPageLayout pageTitle="Indicators"> - <FiltersGlobal> - <QueryBar - dateRangeFrom={timeRange?.from} - dateRangeTo={timeRange?.to} - indexPattern={indexPattern} - filterQuery={filterQuery} - filterManager={filterManager} - filters={filters} - dataTestSubj="iocListPageQueryInput" - displayStyle="detached" - savedQuery={savedQuery} - onRefresh={handleRefresh} - onSubmitQuery={handleSubmitQuery} - onSavedQuery={handleSavedQuery} - onSubmitDateRange={handleSubmitTimeRange} - /> - </FiltersGlobal> - <IndicatorsFilters filterManager={filterManager}> - <IndicatorsBarChartWrapper timeRange={timeRange} indexPattern={indexPattern} /> - <IndicatorsTable - {...indicators} - browserFields={browserFields} - indexPattern={indexPattern} - /> - </IndicatorsFilters> - </DefaultPageLayout> + <FieldTypesProvider> + <DefaultPageLayout pageTitle="Indicators"> + <FiltersGlobal> + <QueryBar + dateRangeFrom={timeRange?.from} + dateRangeTo={timeRange?.to} + indexPattern={indexPattern} + filterQuery={filterQuery} + filterManager={filterManager} + filters={filters} + dataTestSubj="iocListPageQueryInput" + displayStyle="detached" + savedQuery={savedQuery} + onRefresh={handleRefresh} + onSubmitQuery={handleSubmitQuery} + onSavedQuery={handleSavedQuery} + onSubmitDateRange={handleSubmitTimeRange} + /> + </FiltersGlobal> + <IndicatorsFilters filterManager={filterManager}> + <IndicatorsBarChartWrapper timeRange={timeRange} indexPattern={indexPattern} /> + <IndicatorsTable + {...indicators} + browserFields={browserFields} + indexPattern={indexPattern} + /> + </IndicatorsFilters> + </DefaultPageLayout> + </FieldTypesProvider> ); }; diff --git a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in_out/__snapshots__/filter_in_out.test.tsx.snap b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/__snapshots__/filter_in.test.tsx.snap similarity index 76% rename from x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in_out/__snapshots__/filter_in_out.test.tsx.snap rename to x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/__snapshots__/filter_in.test.tsx.snap index 6efed28052d86b..1ceba15da5ad6b 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in_out/__snapshots__/filter_in_out.test.tsx.snap +++ b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/__snapshots__/filter_in.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`<FilterInOut /> should render an empty component (wrong data input) 1`] = ` +exports[`<FilterIn /> should render an empty component (wrong data input) 1`] = ` Object { "asFragment": [Function], "baseElement": <body> @@ -61,7 +61,7 @@ Object { } `; -exports[`<FilterInOut /> should render an empty component (wrong field input) 1`] = ` +exports[`<FilterIn /> should render an empty component (wrong field input) 1`] = ` Object { "asFragment": [Function], "baseElement": <body> @@ -122,30 +122,13 @@ Object { } `; -exports[`<FilterInOut /> should render two Component (for DataGrid use) 1`] = ` +exports[`<FilterIn /> should render one Component (for EuiDataGrid use) 1`] = ` Object { "asFragment": [Function], "baseElement": <body> <div> <div css="[object Object]" - data-test-subj="tiFilterInComponent" - > - <button - class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall" - type="button" - > - <span - aria-hidden="true" - class="euiButtonIcon__icon" - color="inherit" - data-euiicon-type="plusInCircle" - /> - </button> - </div> - <div - css="[object Object]" - data-test-subj="tiFilterOutComponent" > <button class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall" @@ -164,23 +147,6 @@ Object { "container": <div> <div css="[object Object]" - data-test-subj="tiFilterInComponent" - > - <button - class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall" - type="button" - > - <span - aria-hidden="true" - class="euiButtonIcon__icon" - color="inherit" - data-euiicon-type="plusInCircle" - /> - </button> - </div> - <div - css="[object Object]" - data-test-subj="tiFilterOutComponent" > <button class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall" @@ -249,7 +215,7 @@ Object { } `; -exports[`<FilterInOut /> should render two EuiButtonIcon 1`] = ` +exports[`<FilterIn /> should render one EuiButtonIcon 1`] = ` Object { "asFragment": [Function], "baseElement": <body> @@ -257,7 +223,7 @@ Object { <button aria-label="Filter In" class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall" - data-test-subj="tiFilterInIcon" + data-test-subj="abc" type="button" > <span @@ -267,26 +233,13 @@ Object { data-euiicon-type="plusInCircle" /> </button> - <button - aria-label="Filter Out" - class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall" - data-test-subj="tiFilterOutIcon" - type="button" - > - <span - aria-hidden="true" - class="euiButtonIcon__icon" - color="inherit" - data-euiicon-type="minusInCircle" - /> - </button> </div> </body>, "container": <div> <button aria-label="Filter In" class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall" - data-test-subj="tiFilterInIcon" + data-test-subj="abc" type="button" > <span @@ -296,18 +249,106 @@ Object { data-euiicon-type="plusInCircle" /> </button> + </div>, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; + +exports[`<FilterIn /> should render one EuiContextMenuItem (for EuiContextMenu use) 1`] = ` +Object { + "asFragment": [Function], + "baseElement": <body> + <div> + <button + class="euiContextMenuItem euiContextMenuItem--small" + type="button" + > + <span + class="euiContextMenu__itemLayout" + > + <span + class="euiContextMenu__icon" + color="inherit" + data-euiicon-type="plusInCircle" + /> + <span + class="euiContextMenuItem__text" + > + Filter In + </span> + </span> + </button> + </div> + </body>, + "container": <div> <button - aria-label="Filter Out" - class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall" - data-test-subj="tiFilterOutIcon" + class="euiContextMenuItem euiContextMenuItem--small" type="button" > <span - aria-hidden="true" - class="euiButtonIcon__icon" - color="inherit" - data-euiicon-type="minusInCircle" - /> + class="euiContextMenu__itemLayout" + > + <span + class="euiContextMenu__icon" + color="inherit" + data-euiicon-type="plusInCircle" + /> + <span + class="euiContextMenuItem__text" + > + Filter In + </span> + </span> </button> </div>, "debug": [Function], diff --git a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in_out/filter_in_out.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/filter_in.stories.tsx similarity index 85% rename from x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in_out/filter_in_out.stories.tsx rename to x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/filter_in.stories.tsx index da9728f10138d0..1e6c7d0c2614ed 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in_out/filter_in_out.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/filter_in.stories.tsx @@ -8,13 +8,13 @@ import React from 'react'; import { Story } from '@storybook/react'; import { mockIndicatorsFiltersContext } from '../../../../common/mocks/mock_indicators_filters_context'; -import { FilterInOut } from './filter_in_out'; import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator'; import { IndicatorsFiltersContext } from '../../../indicators/context'; +import { FilterIn } from '.'; export default { - component: FilterInOut, - title: 'FilterInOut', + component: FilterIn, + title: 'FilterIn', }; export const Default: Story<void> = () => { @@ -23,7 +23,7 @@ export const Default: Story<void> = () => { return ( <IndicatorsFiltersContext.Provider value={mockIndicatorsFiltersContext}> - <FilterInOut data={mockIndicator} field={mockField} /> + <FilterIn data={mockIndicator} field={mockField} /> </IndicatorsFiltersContext.Provider> ); }; diff --git a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in_out/filter_in_out.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/filter_in.test.tsx similarity index 58% rename from x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in_out/filter_in_out.test.tsx rename to x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/filter_in.test.tsx index db11ca497b5156..788ac9b2e4427f 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in_out/filter_in_out.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/filter_in.test.tsx @@ -7,11 +7,12 @@ import React, { FunctionComponent } from 'react'; import { render } from '@testing-library/react'; -import { FilterInOut, IN_ICON_TEST_ID, OUT_ICON_TEST_ID } from './filter_in_out'; -import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator'; import { EuiButtonIcon } from '@elastic/eui'; +import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator'; import { useIndicatorsFiltersContext } from '../../../indicators/hooks/use_indicators_filters_context'; import { mockIndicatorsFiltersContext } from '../../../../common/mocks/mock_indicators_filters_context'; +import { FilterIn } from '.'; +import { ComponentType } from '../../../../../common/types/component_type'; jest.mock('../../../indicators/hooks/use_indicators_filters_context'); @@ -19,39 +20,50 @@ const mockIndicator: Indicator = generateMockIndicator(); const mockField: string = 'threat.feed.name'; -describe('<FilterInOut />', () => { +const mockTestId: string = 'abc'; + +describe('<FilterIn />', () => { beforeEach(() => { ( useIndicatorsFiltersContext as jest.MockedFunction<typeof useIndicatorsFiltersContext> ).mockReturnValue(mockIndicatorsFiltersContext); }); - it('should render two EuiButtonIcon', () => { - const component = render(<FilterInOut data={mockIndicator} field={mockField} />); + it('should render one EuiButtonIcon', () => { + const component = render( + <FilterIn data={mockIndicator} field={mockField} data-test-subj={mockTestId} /> + ); - expect(component.getByTestId(IN_ICON_TEST_ID)).toBeInTheDocument(); - expect(component.getByTestId(OUT_ICON_TEST_ID)).toBeInTheDocument(); + expect(component.getByTestId(mockTestId)).toBeInTheDocument(); expect(component).toMatchSnapshot(); }); - it('should render two Component (for DataGrid use)', () => { + it('should render one Component (for EuiDataGrid use)', () => { + const mockType: ComponentType = ComponentType.EuiDataGrid; const mockComponent: FunctionComponent = () => <EuiButtonIcon iconType="plusInCircle" />; const component = render( - <FilterInOut data={mockIndicator} field={mockField} Component={mockComponent} /> + <FilterIn data={mockIndicator} field={mockField} type={mockType} as={mockComponent} /> ); expect(component).toMatchSnapshot(); }); + it('should render one EuiContextMenuItem (for EuiContextMenu use)', () => { + const mockType: ComponentType = ComponentType.ContextMenu; + const component = render(<FilterIn data={mockIndicator} field={mockField} type={mockType} />); + + expect(component).toMatchSnapshot(); + }); + it('should render an empty component (wrong data input)', () => { - const component = render(<FilterInOut data={''} field={mockField} />); + const component = render(<FilterIn data={''} field={mockField} />); expect(component).toMatchSnapshot(); }); it('should render an empty component (wrong field input)', () => { - const component = render(<FilterInOut data={mockIndicator} field={''} />); + const component = render(<FilterIn data={mockIndicator} field={''} />); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/filter_in.tsx b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/filter_in.tsx new file mode 100644 index 00000000000000..44893c02c685e5 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/filter_in.tsx @@ -0,0 +1,107 @@ +/* + * 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, { useCallback, VFC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonEmpty, EuiButtonIcon, EuiContextMenuItem } from '@elastic/eui'; +import { Filter } from '@kbn/es-query'; +import { ComponentType } from '../../../../../common/types/component_type'; +import { useIndicatorsFiltersContext } from '../../../indicators/hooks/use_indicators_filters_context'; +import { getIndicatorFieldAndValue } from '../../../indicators/lib/field_value'; +import { FilterIn as FilterInConst, updateFiltersArray } from '../../lib/filter'; +import { EMPTY_VALUE } from '../../../../../common/constants'; +import { Indicator } from '../../../../../common/types/indicator'; +import { useStyles } from './styles'; + +const ICON_TYPE = 'plusInCircle'; +const ICON_TITLE = i18n.translate('xpack.threatIntelligence.queryBar.filterInButton', { + defaultMessage: 'Filter In', +}); + +export interface FilterInProps { + /** + * Value used to filter in/out in the KQL bar. Used in combination with field if is type of {@link Indicator}. + */ + data: Indicator | string; + /** + * Value used to filter in /out in the KQL bar. + */ + field: string; + /** + * Dictates the way the FilterIn component is rendered depending on the situation in which it's used + */ + type?: ComponentType; + /** + * Display component for when the FilterIn component is used within a DataGrid + */ + as?: typeof EuiButtonEmpty | typeof EuiButtonIcon | typeof EuiContextMenuItem; + /** + * Used for unit and e2e tests. + */ + ['data-test-subj']?: string; +} + +/** + * Retrieves the indicator's field and value, then creates a new {@link Filter} and adds it to the {@link FilterManager}. + * + * The component has 3 renders depending on where it's used: within a EuiContextMenu, a EuiDataGrid or not. + * + * @returns filter in button + */ +export const FilterIn: VFC<FilterInProps> = ({ data, field, type, as: Component, ...props }) => { + const styles = useStyles(); + + const { filterManager } = useIndicatorsFiltersContext(); + + const { key, value } = + typeof data === 'string' ? { key: field, value: data } : getIndicatorFieldAndValue(data, field); + + const filterIn = useCallback((): void => { + const existingFilters = filterManager.getFilters(); + const newFilters: Filter[] = updateFiltersArray(existingFilters, key, value, FilterInConst); + filterManager.setFilters(newFilters); + }, [filterManager, key, value]); + + if (!value || value === EMPTY_VALUE || !key) { + return <></>; + } + + if (type === ComponentType.EuiDataGrid) { + return ( + <div {...props} css={styles.button}> + {/* @ts-ignore*/} + <Component aria-label={ICON_TITLE} iconType={ICON_TYPE} onClick={filterIn} /> + </div> + ); + } + + if (type === ComponentType.ContextMenu) { + return ( + <EuiContextMenuItem + key="addToTimeline" + icon="plusInCircle" + size="s" + onClick={filterIn} + {...props} + > + Filter In + </EuiContextMenuItem> + ); + } + + return ( + <EuiButtonIcon + aria-label={ICON_TITLE} + iconType={ICON_TYPE} + iconSize="s" + size="xs" + color="primary" + onClick={filterIn} + {...props} + /> + ); +}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in_out/index.tsx b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/index.tsx similarity index 88% rename from x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in_out/index.tsx rename to x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/index.tsx index e0e44b90b824f0..a1a32358e49be5 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in_out/index.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/index.tsx @@ -5,4 +5,4 @@ * 2.0. */ -export * from './filter_in_out'; +export * from './filter_in'; diff --git a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in_out/styles.ts b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/styles.ts similarity index 100% rename from x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in_out/styles.ts rename to x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/styles.ts diff --git a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in_out/filter_in_out.tsx b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in_out/filter_in_out.tsx deleted file mode 100644 index 56088c953ef08c..00000000000000 --- a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in_out/filter_in_out.tsx +++ /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 React, { useCallback, VFC } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; -import { Filter } from '@kbn/es-query'; -import { useIndicatorsFiltersContext } from '../../../indicators/hooks/use_indicators_filters_context'; -import { getIndicatorFieldAndValue } from '../../../indicators/lib/field_value'; -import { FilterIn, FilterOut, updateFiltersArray } from '../../lib/filter'; -import { EMPTY_VALUE } from '../../../../../common/constants'; -import { Indicator } from '../../../../../common/types/indicator'; -import { useStyles } from './styles'; - -export const IN_ICON_TEST_ID = 'tiFilterInIcon'; -export const OUT_ICON_TEST_ID = 'tiFilterOutIcon'; -export const IN_COMPONENT_TEST_ID = 'tiFilterInComponent'; -export const OUT_COMPONENT_TEST_ID = 'tiFilterOutComponent'; - -const IN_ICON_TYPE = 'plusInCircle'; -const IN_ICON_TITLE = i18n.translate('xpack.threatIntelligence.queryBar.filterInButton', { - defaultMessage: 'Filter In', -}); -const OUT_ICON_TYPE = 'minusInCircle'; -const OUT_ICON_TITLE = i18n.translate('xpack.threatIntelligence.queryBar.filterOutButton', { - defaultMessage: 'Filter Out', -}); - -export interface FilterInOutProps { - /** - * Value used to filter in/out in the KQL bar. Used in combination with field if is type of {@link Indicator}. - */ - data: Indicator | string; - /** - * Value used to filter in /out in the KQL bar. - */ - field: string; - /** - * Display component for when the FilterIn component is used within a DataGrid - */ - Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon; -} - -/** - * Retrieves the indicator's field and value, then creates a new {@link Filter} and adds it to the {@link FilterManager}. - */ -export const FilterInOut: VFC<FilterInOutProps> = ({ data, field, Component }) => { - const styles = useStyles(); - - const { filterManager } = useIndicatorsFiltersContext(); - - const { key, value } = - typeof data === 'string' ? { key: field, value: data } : getIndicatorFieldAndValue(data, field); - - const filterIn = useCallback((): void => { - const existingFilters = filterManager.getFilters(); - const newFilters: Filter[] = updateFiltersArray(existingFilters, key, value, FilterIn); - filterManager.setFilters(newFilters); - }, [filterManager, key, value]); - - const filterOut = useCallback(() => { - const existingFilters: Filter[] = filterManager.getFilters(); - const newFilters: Filter[] = updateFiltersArray(existingFilters, key, value, FilterOut); - filterManager.setFilters(newFilters); - }, [filterManager, key, value]); - - if (!value || value === EMPTY_VALUE || !key) { - return <></>; - } - - return Component ? ( - <> - <div data-test-subj={IN_COMPONENT_TEST_ID} css={styles.button}> - <Component aria-label={IN_ICON_TITLE} iconType={IN_ICON_TYPE} onClick={filterIn} /> - </div> - <div data-test-subj={OUT_COMPONENT_TEST_ID} css={styles.button}> - <Component - data-test-subj={IN_ICON_TEST_ID} - aria-label={OUT_ICON_TITLE} - iconType={OUT_ICON_TYPE} - onClick={filterOut} - /> - </div> - </> - ) : ( - <> - <EuiButtonIcon - data-test-subj={IN_ICON_TEST_ID} - aria-label={IN_ICON_TITLE} - iconType={IN_ICON_TYPE} - iconSize="s" - size="xs" - color="primary" - onClick={filterIn} - /> - <EuiButtonIcon - data-test-subj={OUT_ICON_TEST_ID} - aria-label={OUT_ICON_TITLE} - iconType={OUT_ICON_TYPE} - iconSize="s" - size="xs" - color="primary" - onClick={filterOut} - /> - </> - ); -}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/__snapshots__/filter_out.test.tsx.snap b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/__snapshots__/filter_out.test.tsx.snap new file mode 100644 index 00000000000000..5ace1d59f4bd8c --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/__snapshots__/filter_out.test.tsx.snap @@ -0,0 +1,406 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`<FilterOut /> should render an empty component (wrong data input) 1`] = ` +Object { + "asFragment": [Function], + "baseElement": <body> + <div /> + </body>, + "container": <div />, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; + +exports[`<FilterOut /> should render an empty component (wrong field input) 1`] = ` +Object { + "asFragment": [Function], + "baseElement": <body> + <div /> + </body>, + "container": <div />, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; + +exports[`<FilterOut /> should render one Component (for EuiDataGrid use) 1`] = ` +Object { + "asFragment": [Function], + "baseElement": <body> + <div> + <div + css="[object Object]" + > + <button + class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall" + type="button" + > + <span + aria-hidden="true" + class="euiButtonIcon__icon" + color="inherit" + data-euiicon-type="plusInCircle" + /> + </button> + </div> + </div> + </body>, + "container": <div> + <div + css="[object Object]" + > + <button + class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall" + type="button" + > + <span + aria-hidden="true" + class="euiButtonIcon__icon" + color="inherit" + data-euiicon-type="plusInCircle" + /> + </button> + </div> + </div>, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; + +exports[`<FilterOut /> should render one EuiButtonIcon 1`] = ` +Object { + "asFragment": [Function], + "baseElement": <body> + <div> + <button + aria-label="Filter Out" + class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall" + data-test-subj="abc" + type="button" + > + <span + aria-hidden="true" + class="euiButtonIcon__icon" + color="inherit" + data-euiicon-type="minusInCircle" + /> + </button> + </div> + </body>, + "container": <div> + <button + aria-label="Filter Out" + class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall" + data-test-subj="abc" + type="button" + > + <span + aria-hidden="true" + class="euiButtonIcon__icon" + color="inherit" + data-euiicon-type="minusInCircle" + /> + </button> + </div>, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; + +exports[`<FilterOut /> should render one EuiContextMenuItem (for EuiContextMenu use) 1`] = ` +Object { + "asFragment": [Function], + "baseElement": <body> + <div> + <button + class="euiContextMenuItem euiContextMenuItem--small" + type="button" + > + <span + class="euiContextMenu__itemLayout" + > + <span + class="euiContextMenu__icon" + color="inherit" + data-euiicon-type="minusInCircle" + /> + <span + class="euiContextMenuItem__text" + > + Filter Out + </span> + </span> + </button> + </div> + </body>, + "container": <div> + <button + class="euiContextMenuItem euiContextMenuItem--small" + type="button" + > + <span + class="euiContextMenu__itemLayout" + > + <span + class="euiContextMenu__icon" + color="inherit" + data-euiicon-type="minusInCircle" + /> + <span + class="euiContextMenuItem__text" + > + Filter Out + </span> + </span> + </button> + </div>, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; diff --git a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/filter_out.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/filter_out.stories.tsx new file mode 100644 index 00000000000000..a2f0061d36a94c --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/filter_out.stories.tsx @@ -0,0 +1,29 @@ +/* + * 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 from 'react'; +import { Story } from '@storybook/react'; +import { mockIndicatorsFiltersContext } from '../../../../common/mocks/mock_indicators_filters_context'; +import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator'; +import { IndicatorsFiltersContext } from '../../../indicators/context'; +import { FilterOut } from '.'; + +export default { + component: FilterOut, + title: 'FilterOut', +}; + +export const Default: Story<void> = () => { + const mockIndicator: Indicator = generateMockIndicator(); + const mockField: string = 'threat.feed.name'; + + return ( + <IndicatorsFiltersContext.Provider value={mockIndicatorsFiltersContext}> + <FilterOut data={mockIndicator} field={mockField} /> + </IndicatorsFiltersContext.Provider> + ); +}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/filter_out.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/filter_out.test.tsx new file mode 100644 index 00000000000000..6a65ff5036921b --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/filter_out.test.tsx @@ -0,0 +1,71 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { render } from '@testing-library/react'; +import { EuiButtonIcon } from '@elastic/eui'; +import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator'; +import { useIndicatorsFiltersContext } from '../../../indicators/hooks/use_indicators_filters_context'; +import { mockIndicatorsFiltersContext } from '../../../../common/mocks/mock_indicators_filters_context'; +import { FilterOut } from '.'; +import { ComponentType } from '../../../../../common/types/component_type'; + +jest.mock('../../../indicators/hooks/use_indicators_filters_context'); + +const mockIndicator: Indicator = generateMockIndicator(); + +const mockField: string = 'threat.feed.name'; + +const mockTestId: string = 'abc'; + +describe('<FilterOut />', () => { + beforeEach(() => { + ( + useIndicatorsFiltersContext as jest.MockedFunction<typeof useIndicatorsFiltersContext> + ).mockReturnValue(mockIndicatorsFiltersContext); + }); + + it('should render one EuiButtonIcon', () => { + const component = render( + <FilterOut data={mockIndicator} field={mockField} data-test-subj={mockTestId} /> + ); + + expect(component.getByTestId(mockTestId)).toBeInTheDocument(); + expect(component).toMatchSnapshot(); + }); + + it('should render one Component (for EuiDataGrid use)', () => { + const mockType: ComponentType = ComponentType.EuiDataGrid; + const mockComponent: FunctionComponent = () => <EuiButtonIcon iconType="plusInCircle" />; + + const component = render( + <FilterOut data={mockIndicator} field={mockField} type={mockType} as={mockComponent} /> + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render one EuiContextMenuItem (for EuiContextMenu use)', () => { + const mockType: ComponentType = ComponentType.ContextMenu; + + const component = render(<FilterOut data={mockIndicator} field={mockField} type={mockType} />); + + expect(component).toMatchSnapshot(); + }); + + it('should render an empty component (wrong data input)', () => { + const component = render(<FilterOut data={''} field={mockField} />); + + expect(component).toMatchSnapshot(); + }); + + it('should render an empty component (wrong field input)', () => { + const component = render(<FilterOut data={mockIndicator} field={''} />); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/filter_out.tsx b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/filter_out.tsx new file mode 100644 index 00000000000000..dd7a644d43fe21 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/filter_out.tsx @@ -0,0 +1,107 @@ +/* + * 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, { useCallback, VFC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonEmpty, EuiButtonIcon, EuiContextMenuItem } from '@elastic/eui'; +import { Filter } from '@kbn/es-query'; +import { ComponentType } from '../../../../../common/types/component_type'; +import { useIndicatorsFiltersContext } from '../../../indicators/hooks/use_indicators_filters_context'; +import { getIndicatorFieldAndValue } from '../../../indicators/lib/field_value'; +import { FilterOut as FilterOutConst, updateFiltersArray } from '../../lib/filter'; +import { EMPTY_VALUE } from '../../../../../common/constants'; +import { Indicator } from '../../../../../common/types/indicator'; +import { useStyles } from './styles'; + +const ICON_TYPE = 'minusInCircle'; +const ICON_TITLE = i18n.translate('xpack.threatIntelligence.queryBar.filterOutButton', { + defaultMessage: 'Filter Out', +}); + +export interface FilterOutProps { + /** + * Value used to filter in/out in the KQL bar. Used in combination with field if is type of {@link Indicator}. + */ + data: Indicator | string; + /** + * Value used to filter in /out in the KQL bar. + */ + field: string; + /** + * Dictates the way the FilterOut component is rendered depending on the situation in which it's used + */ + type?: ComponentType; + /** + * Display component for when the FilterIn component is used within a DataGrid + */ + as?: typeof EuiButtonEmpty | typeof EuiButtonIcon; + /** + * Used for unit and e2e tests. + */ + ['data-test-subj']?: string; +} + +/** + * Retrieves the indicator's field and value, then creates a new {@link Filter} and adds it to the {@link FilterManager}. + * + * The component has 3 renders depending on where it's used: within a EuiContextMenu, a EuiDataGrid or not. + * + * @returns filter out button + */ +export const FilterOut: VFC<FilterOutProps> = ({ data, field, type, as: Component, ...props }) => { + const styles = useStyles(); + + const { filterManager } = useIndicatorsFiltersContext(); + + const { key, value } = + typeof data === 'string' ? { key: field, value: data } : getIndicatorFieldAndValue(data, field); + + const filterOut = useCallback(() => { + const existingFilters: Filter[] = filterManager.getFilters(); + const newFilters: Filter[] = updateFiltersArray(existingFilters, key, value, FilterOutConst); + filterManager.setFilters(newFilters); + }, [filterManager, key, value]); + + if (!value || value === EMPTY_VALUE || !key) { + return <></>; + } + + if (type === ComponentType.EuiDataGrid) { + return ( + <div {...props} css={styles.button}> + {/* @ts-ignore*/} + <Component aria-label={ICON_TITLE} iconType={ICON_TYPE} onClick={filterOut} /> + </div> + ); + } + + if (type === ComponentType.ContextMenu) { + return ( + <EuiContextMenuItem + key="filterOut" + icon="minusInCircle" + size="s" + onClick={filterOut} + {...props} + > + Filter Out + </EuiContextMenuItem> + ); + } + + return ( + <EuiButtonIcon + aria-label={ICON_TITLE} + iconType={ICON_TYPE} + iconSize="s" + size="xs" + color="primary" + onClick={filterOut} + {...props} + /> + ); +}; diff --git a/x-pack/plugins/ml/__mocks__/shared_imports.ts b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/index.tsx similarity index 87% rename from x-pack/plugins/ml/__mocks__/shared_imports.ts rename to x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/index.tsx index 43494fea6992a4..8dfa9ada683289 100644 --- a/x-pack/plugins/ml/__mocks__/shared_imports.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/index.tsx @@ -5,4 +5,4 @@ * 2.0. */ -export const XJsonMode = jest.fn(); +export * from './filter_out'; diff --git a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/styles.ts b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/styles.ts new file mode 100644 index 00000000000000..4f7814eb793ccb --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/styles.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CSSObject } from '@emotion/react'; + +export const useStyles = () => { + const button: CSSObject = { + display: 'inline-flex', + }; + + return { + button, + }; +}; diff --git a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/query_bar/query_bar.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/query_bar/query_bar.test.tsx index bb05a23feb1746..2ab2795e138bdb 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/query_bar/query_bar.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/query_bar/query_bar.test.tsx @@ -13,9 +13,8 @@ import userEvent from '@testing-library/user-event'; import { FilterManager } from '@kbn/data-plugin/public'; import { coreMock } from '@kbn/core/public/mocks'; -import { TestProvidersComponent, unifiedSearch } from '../../../../common/mocks/test_providers'; +import { TestProvidersComponent } from '../../../../common/mocks/test_providers'; import { getByTestSubj } from '../../../../../common/test/utils'; -import { setAutocomplete } from '@kbn/unified-search-plugin/public/services'; const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; @@ -27,10 +26,6 @@ describe('QueryBar ', () => { const onSavedQuery = jest.fn(); const onChangedQuery = jest.fn(); - beforeEach(() => { - setAutocomplete(unifiedSearch.autocomplete); - }); - beforeEach(async () => { await act(async () => { render( diff --git a/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/add_to_timeline.tsx b/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/add_to_timeline.tsx index c677a7613e3e62..792f1f2784b0e3 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/add_to_timeline.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/add_to_timeline.tsx @@ -5,10 +5,12 @@ * 2.0. */ -import React, { VFC } from 'react'; +import React, { useRef, VFC } from 'react'; import { DataProvider, QueryOperator } from '@kbn/timelines-plugin/common'; import { AddToTimelineButtonProps } from '@kbn/timelines-plugin/public'; import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui/src/components/button'; +import { EuiContextMenuItem } from '@elastic/eui'; +import { ComponentType } from '../../../../../common/types/component_type'; import { getIndicatorFieldAndValue } from '../../../indicators/lib/field_value'; import { EMPTY_VALUE } from '../../../../../common/constants'; import { useKibana } from '../../../../hooks/use_kibana'; @@ -24,14 +26,18 @@ export interface AddToTimelineProps { * Value passed to the timeline. */ field: string; + /** + * Dictates the way the FilterIn component is rendered depending on the situation in which it's used + */ + type?: ComponentType; /** * Only used with `EuiDataGrid` (see {@link AddToTimelineButtonProps}). */ - component?: typeof EuiButtonEmpty | typeof EuiButtonIcon; + as?: typeof EuiButtonEmpty | typeof EuiButtonIcon; /** - * Used as `data-test-subj` value for e2e tests. + * Used for unit and e2e tests. */ - testId?: string; + ['data-test-subj']?: string; } /** @@ -40,11 +46,15 @@ export interface AddToTimelineProps { * Leverages the built-in functionality retrieves from the timeLineService (see ThreatIntelligenceSecuritySolutionContext in x-pack/plugins/threat_intelligence/public/types.ts) * Clicking on the button will add a key-value pair to an Untitled timeline. * + * The component has 2 renders depending on where it's used: within a EuiContextMenu or not. + * * @returns add to timeline button or an empty component. */ -export const AddToTimeline: VFC<AddToTimelineProps> = ({ data, field, component, testId }) => { +export const AddToTimeline: VFC<AddToTimelineProps> = ({ data, field, type, as, ...props }) => { const styles = useStyles(); + const contextMenuRef = useRef<HTMLButtonElement>(null); + const addToTimelineButton = useKibana().services.timelines.getHoverActions().getAddToTimelineButton; @@ -78,10 +88,33 @@ export const AddToTimeline: VFC<AddToTimelineProps> = ({ data, field, component, field: key, ownFocus: false, }; - if (component) addToTimelineProps.Component = component; + + // Use case is for the barchart legend (for example). + // We can't use the addToTimelineButton directly because the UI doesn't work in a EuiContextMenu. + // We hide it and use the defaultFocusedButtonRef props to programmatically click it. + if (type === ComponentType.ContextMenu) { + addToTimelineProps.defaultFocusedButtonRef = contextMenuRef; + + return ( + <> + <div css={styles.displayNone}>{addToTimelineButton(addToTimelineProps)}</div> + <EuiContextMenuItem + key="addToTimeline" + icon="timeline" + size="s" + onClick={() => contextMenuRef.current?.click()} + {...props} + > + Add to Timeline + </EuiContextMenuItem> + </> + ); + } + + if (as) addToTimelineProps.Component = as; return ( - <div data-test-subj={testId} css={styles.button}> + <div {...props} css={styles.inlineFlex}> {addToTimelineButton(addToTimelineProps)} </div> ); diff --git a/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/styles.ts b/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/styles.ts index 4f7814eb793ccb..d4f13c215a7a74 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/styles.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline/styles.ts @@ -8,11 +8,16 @@ import { CSSObject } from '@emotion/react'; export const useStyles = () => { - const button: CSSObject = { + const inlineFlex: CSSObject = { display: 'inline-flex', }; + const displayNone: CSSObject = { + display: 'none', + }; + return { - button, + inlineFlex, + displayNone, }; }; diff --git a/x-pack/plugins/threat_intelligence/scripts/generate_indicators.js b/x-pack/plugins/threat_intelligence/scripts/generate_indicators.js new file mode 100644 index 00000000000000..44e0ad166353c4 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/scripts/generate_indicators.js @@ -0,0 +1,121 @@ +/* + * 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. + */ + +const { Client } = require('@elastic/elasticsearch'); +const faker = require('faker'); + +const THREAT_INDEX = 'logs-ti'; + +/** Drop the index first? */ +const CLEANUP_FIRST = true; + +/** Adjust this to alter the threat number */ +const HOW_MANY_THREATS = 1_000_000; + +/** Feed names */ +const FEED_NAMES = ['Max', 'Philippe', 'Lukasz', 'Fernanda', 'Drew']; + +/** + * Customizing this is optional, you can skip it + */ +const CHUNK_SIZE = 10_000; +const TO_GENERATE = HOW_MANY_THREATS; + +const client = new Client({ + node: 'http://localhost:9200', + auth: { + username: 'elastic', + password: 'changeme', + }, +}); + +const main = async () => { + if (await client.indices.exists({ index: THREAT_INDEX })) { + if (CLEANUP_FIRST) { + console.log(`deleting index "${THREAT_INDEX}"`); + + await client.indices.delete({ index: THREAT_INDEX }); + + await client.indices.create({ + index: THREAT_INDEX, + mappings: { + properties: { + 'threat.indicator.type': { + type: 'keyword', + }, + 'threat.feed.name': { + type: 'keyword', + }, + 'threat.indicator.url.original': { + type: 'keyword', + }, + 'threat.indicator.first_seen': { + type: 'date', + }, + '@timestamp': { + type: 'date', + }, + }, + }, + }); + } else { + console.info( + `!!! appending to existing index "${THREAT_INDEX}" !!! (because CLEANUP_FIRST is set to true)` + ); + } + } else if (!CLEANUP_FIRST) { + throw new Error( + `index "${THREAT_INDEX}" does not exist. run this script with CLEANUP_FIRST set to true or create it some other way first.` + ); + } + + let pendingCount = TO_GENERATE; + + // When there are threats to generate + while (pendingCount) { + const operations = []; + + for (let i = 0; i < CHUNK_SIZE; i++) { + const RANDOM_OFFSET_WITHIN_ONE_MONTH = Math.floor(Math.random() * 3600 * 24 * 30 * 1000); + + const timestamp = new Date(Date.now() - RANDOM_OFFSET_WITHIN_ONE_MONTH).toISOString(); + + operations.push( + ...[ + { create: { _index: THREAT_INDEX } }, + { + '@timestamp': timestamp, + 'threat.indicator.first_seen': timestamp, + 'threat.feed.name': FEED_NAMES[Math.ceil(Math.random() * FEED_NAMES.length) - 1], + 'threat.indicator.type': 'url', + 'threat.indicator.url.original': faker.internet.url(), + 'event.type': 'indicator', + 'event.category': 'threat', + }, + ] + ); + + pendingCount--; + + if (!pendingCount) { + break; + } + } + + await client.bulk({ operations }); + + console.info( + `${operations.length / 2} new threats indexed, ${ + pendingCount ? `${pendingCount} pending` : 'complete' + }` + ); + } + + console.info('done, run your tests would you?'); +}; + +main(); diff --git a/x-pack/plugins/timelines/public/components/utils/keury/index.ts b/x-pack/plugins/timelines/public/components/utils/keury/index.ts index 240800c0af75f3..8f03c2f9ef20ef 100644 --- a/x-pack/plugins/timelines/public/components/utils/keury/index.ts +++ b/x-pack/plugins/timelines/public/components/utils/keury/index.ts @@ -71,7 +71,7 @@ export const convertToBuildEsQuery = ({ filters, }: { config: EsQueryConfig; - indexPattern: DataViewBase; + indexPattern: DataViewBase | undefined; queries: Query[]; filters: Filter[]; }): [string, undefined] | [undefined, Error] => { diff --git a/x-pack/plugins/timelines/public/container/index.tsx b/x-pack/plugins/timelines/public/container/index.tsx index ef82aa527bab3f..15e72e7aed2bba 100644 --- a/x-pack/plugins/timelines/public/container/index.tsx +++ b/x-pack/plugins/timelines/public/container/index.tsx @@ -16,7 +16,6 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { isCompleteResponse, isErrorResponse } from '@kbn/data-plugin/common'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; import { clearEventsLoading, clearEventsDeleted, @@ -43,7 +42,7 @@ import type { KueryFilterQueryKind } from '../../common/types/timeline'; import { useAppToasts } from '../hooks/use_app_toasts'; import { TimelineId } from '../store/t_grid/types'; import * as i18n from './translations'; -import { TimelinesStartPlugins } from '../types'; +import { getSearchTransactionName, useStartTransaction } from '../lib/apm/use_start_transaction'; export type InspectResponse = Inspect & { response: string[] }; @@ -118,14 +117,16 @@ export const initSortDefault = [ ]; const useApmTracking = (timelineId: string) => { - const { apm } = useKibana<TimelinesStartPlugins>().services; + const { startTransaction } = useStartTransaction(); const startTracking = useCallback(() => { // Create the transaction, the managed flag is turned off to prevent it from being polluted by non-related automatic spans. // The managed flag can be turned on to investigate high latency requests in APM. // However, note that by enabling the managed flag, the transaction trace may be distorted by other requests information. - const transaction = apm?.startTransaction(`Timeline search ${timelineId}`, 'http-request', { - managed: false, + const transaction = startTransaction({ + name: getSearchTransactionName(timelineId), + type: 'http-request', + options: { managed: false }, }); // Create a blocking span to control the transaction time and prevent it from closing automatically with partial batch responses. // The blocking span needs to be ended manually when the batched request finishes. @@ -136,7 +137,7 @@ const useApmTracking = (timelineId: string) => { span?.end(); }, }; - }, [apm, timelineId]); + }, [startTransaction, timelineId]); return { startTracking }; }; diff --git a/x-pack/plugins/timelines/public/hooks/use_bulk_action_items.tsx b/x-pack/plugins/timelines/public/hooks/use_bulk_action_items.tsx index 02abe3a229df41..202a648916081e 100644 --- a/x-pack/plugins/timelines/public/hooks/use_bulk_action_items.tsx +++ b/x-pack/plugins/timelines/public/hooks/use_bulk_action_items.tsx @@ -13,6 +13,8 @@ import type { AlertStatus, BulkActionsProps } from '../../common/types/timeline' import { useUpdateAlertsStatus } from '../container/use_update_alerts'; import { useAppToasts } from './use_app_toasts'; import { STANDALONE_ID } from '../components/t_grid/standalone'; +import { useStartTransaction } from '../lib/apm/use_start_transaction'; +import { APM_USER_INTERACTIONS } from '../lib/apm/constants'; export const getUpdateAlertsQuery = (eventIds: Readonly<string[]>) => { return { bool: { filter: { terms: { _id: eventIds } } } }; @@ -33,6 +35,7 @@ export const useBulkActionItems = ({ }: BulkActionsProps) => { const { updateAlertStatus } = useUpdateAlertsStatus(timelineId !== STANDALONE_ID); const { addSuccess, addError, addWarning } = useAppToasts(); + const { startTransaction } = useStartTransaction(); const onAlertStatusUpdateSuccess = useCallback( (updated: number, conflicts: number, newStatus: AlertStatus) => { @@ -88,6 +91,14 @@ export const useBulkActionItems = ({ const onClickUpdate = useCallback( async (status: AlertStatus) => { + if (query) { + startTransaction({ name: APM_USER_INTERACTIONS.BULK_QUERY_STATUS_UPDATE }); + } else if (eventIds.length > 1) { + startTransaction({ name: APM_USER_INTERACTIONS.BULK_STATUS_UPDATE }); + } else { + startTransaction({ name: APM_USER_INTERACTIONS.STATUS_UPDATE }); + } + try { setEventsLoading({ eventIds, isLoading: true }); @@ -120,6 +131,7 @@ export const useBulkActionItems = ({ setEventsDeleted, onAlertStatusUpdateSuccess, onAlertStatusUpdateFailure, + startTransaction, ] ); diff --git a/x-pack/plugins/timelines/public/lib/apm/constants.ts b/x-pack/plugins/timelines/public/lib/apm/constants.ts new file mode 100644 index 00000000000000..6b8036f2d2393a --- /dev/null +++ b/x-pack/plugins/timelines/public/lib/apm/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const APM_USER_INTERACTIONS = { + BULK_QUERY_STATUS_UPDATE: 'Timeline bulkQueryStatusUpdate', + BULK_STATUS_UPDATE: 'Timeline bulkStatusUpdate', + STATUS_UPDATE: 'Timeline statusUpdate', +} as const; diff --git a/x-pack/plugins/timelines/public/lib/apm/types.ts b/x-pack/plugins/timelines/public/lib/apm/types.ts new file mode 100644 index 00000000000000..eb52ab17b2f94b --- /dev/null +++ b/x-pack/plugins/timelines/public/lib/apm/types.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { APM_USER_INTERACTIONS } from './constants'; + +export type ApmUserInteractionName = + typeof APM_USER_INTERACTIONS[keyof typeof APM_USER_INTERACTIONS]; + +export type ApmSearchRequestName = `Timeline search ${string}`; + +export type ApmTransactionName = ApmSearchRequestName | ApmUserInteractionName; diff --git a/x-pack/plugins/timelines/public/lib/apm/use_start_transaction.ts b/x-pack/plugins/timelines/public/lib/apm/use_start_transaction.ts new file mode 100644 index 00000000000000..fa47db412e467f --- /dev/null +++ b/x-pack/plugins/timelines/public/lib/apm/use_start_transaction.ts @@ -0,0 +1,36 @@ +/* + * 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 { useCallback } from 'react'; +import type { TransactionOptions } from '@elastic/apm-rum'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { TimelinesStartPlugins } from '../../types'; +import type { ApmSearchRequestName, ApmTransactionName } from './types'; + +const DEFAULT_TRANSACTION_OPTIONS: TransactionOptions = { managed: true }; + +interface StartTransactionOptions { + name: ApmTransactionName; + type?: string; + options?: TransactionOptions; +} + +export const useStartTransaction = () => { + const { apm } = useKibana<TimelinesStartPlugins>().services; + + const startTransaction = useCallback( + ({ name, type = 'user-interaction', options }: StartTransactionOptions) => { + return apm?.startTransaction(name, type, options ?? DEFAULT_TRANSACTION_OPTIONS); + }, + [apm] + ); + + return { startTransaction }; +}; + +export const getSearchTransactionName = (timelineId: string): ApmSearchRequestName => + `Timeline search ${timelineId}`; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 43b3cbacf2a8df..a26241e1c7d21c 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -4953,7 +4953,6 @@ "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesFilterPlaceholderText": "Variables de filtre", "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText": "Aide", "unifiedSearch.filter.filterBar.filterItemBadgeIconAriaLabel": "Supprimer {filter}", - "unifiedSearch.filter.filterBar.labelErrorInfo": "Modèle d'indexation {indexPattern} introuvable", "unifiedSearch.filter.filterBar.labelWarningInfo": "Le champ {fieldName} n'existe pas dans la vue en cours.", "unifiedSearch.kueryAutocomplete.andOperatorDescription": "Nécessite que {bothArguments} soient ''vrai''.", "unifiedSearch.kueryAutocomplete.equalOperatorDescription": "{equals} une certaine valeur", @@ -5026,7 +5025,6 @@ "unifiedSearch.filter.filterBar.includeFilterButtonLabel": "Inclure les résultats", "unifiedSearch.filter.filterBar.indexPatternSelectPlaceholder": "Sélectionner une vue de données", "unifiedSearch.filter.filterBar.invalidDateFormatProvidedErrorMessage": "Format de date non valide fourni", - "unifiedSearch.filter.filterBar.labelErrorText": "Erreur", "unifiedSearch.filter.filterBar.labelWarningText": "Avertissement", "unifiedSearch.filter.filterBar.negatedFilterPrefix": "NON ", "unifiedSearch.filter.filterBar.pinFilterButtonLabel": "Épingler dans toutes les applications", @@ -15037,7 +15035,6 @@ "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshot.repositoryHelpText": "Chaque phase utilise le même référentiel de snapshot.", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshot.storageHelpText": "Type de snapshot installé pour le snapshot qu’il est possible de rechercher. Il s'agit d'une option avancée. Ne la modifiez que si vous savez ce que vous faites.", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshot.storageLabel": "Stockage", - "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotCalloutBody": "Les actions Forcer la fusion, Réduire et Lecture seule ne sont pas autorisées lors de la conversion des données en index entièrement installé dans cette phase.", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutBody": "Licence Enterprise obligatoire pour créer un snapshot qu’il est possible de rechercher.", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutTitle": "Licence Enterprise requise", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoFieldLabel": "Référentiel de snapshot", @@ -25210,7 +25207,6 @@ "xpack.security.unauthenticated.pageTitle": "Impossible de vous connecter", "xpack.security.users.breadcrumb": "Utilisateurs", "xpack.security.users.editUserPage.createBreadcrumb": "Créer", - "xpack.securitySolution.alertDetails.overview.hostDataTooltipContent": "La classification des risques n’est affichée que lorsqu’elle est disponible pour un hôte. Vérifiez que {hostsRiskScoreDocumentationLink} est activé dans votre environnement.", "xpack.securitySolution.alertDetails.overview.insights_related_alerts_by_source_event_count": "{count} {count, plural, =1 {alerte} other {alertes}} par événement source", "xpack.securitySolution.alertDetails.overview.insights_related_cases_found_content": "Cette alerte a été détectée dans {caseCount}", "xpack.securitySolution.alertDetails.overview.insights_related_cases_found_content_count": "{caseCount} {caseCount, plural, =0 {cas.} =1 {cas :} other {cas :}}", @@ -25449,7 +25445,7 @@ "xpack.securitySolution.eventsViewer.unit": "{totalCount, plural, =1 {événement} other {événements}}", "xpack.securitySolution.exceptions.dissasociateListSuccessText": "La liste d'exceptions ({id}) a été retirée avec succès", "xpack.securitySolution.exceptions.exceptionItem.showCommentsLabel": "Afficher {comments, plural, =1 {commentaire} other {commentaires}} ({comments})", - "xpack.securitySolution.exceptions.failedLoadPolicies": "Une erreur s'est produite lors du chargement des politiques : \"{error}\"", + "xpack.securitySolution.exceptions.failedLoadPolicies": "Une erreur s'est produite lors du chargement des politiques : \"{error}\"", "xpack.securitySolution.exceptions.fetch404Error": "La liste d'exceptions associée ({listId}) n'existe plus. Veuillez retirer la liste d'exceptions manquante pour ajouter des exceptions supplémentaires à la règle de détection.", "xpack.securitySolution.exceptions.hideCommentsLabel": "Masquer ({comments}) {comments, plural, =1 {commentaire} other {commentaires}}", "xpack.securitySolution.exceptions.referenceModalDescription": "Cette liste d'exceptions est associée à ({referenceCount}) {referenceCount, plural, =1 {règle} other {règles}}. Le retrait de cette liste d'exceptions supprimera également sa référence des règles associées.", @@ -25466,7 +25462,6 @@ "xpack.securitySolution.hostIsolationExceptions.flyoutCreateSubmitSuccess": "\"{name}\" a été ajouté à votre liste d'exceptions d'isolation de l'hôte.", "xpack.securitySolution.hostIsolationExceptions.flyoutEditSubmitSuccess": "\"{name}\" a été mis à jour.", "xpack.securitySolution.hostIsolationExceptions.showingTotal": "Affichage de {total} {total, plural, one {exception d'isolation de l'hôte} other {exceptions d'isolation de l'hôte}}", - "xpack.securitySolution.hosts.hostRiskInformation.learnMore": "Pour en savoir plus sur le risque de l'hôte, cliquez {hostsRiskScoreDocumentationLink}", "xpack.securitySolution.hosts.navigaton.eventsUnit": "{totalCount, plural, =1 {événement} other {événements}}", "xpack.securitySolution.hostsRiskTable.filteredHostsTitle": "Afficher les hôtes à risque {severity}", "xpack.securitySolution.hostsTable.rows": "{numRows} {numRows, plural, =0 {ligne} =1 {ligne} other {lignes}}", @@ -25519,7 +25514,6 @@ "xpack.securitySolution.overview.ctiDashboardSubtitle": "Affichage : {totalCount} {totalCount, plural, one {indicateur} other {indicateurs}}", "xpack.securitySolution.overview.overviewHost.hostsSubtitle": "Affichage de : {formattedHostEventsCount} {hostEventsCount, plural, one {événement} other {événements}}", "xpack.securitySolution.overview.overviewNetwork.networkSubtitle": "Affichage de : {formattedNetworkEventsCount} {networkEventsCount, plural, one {événement} other {événements}}", - "xpack.securitySolution.overview.riskyHostsDashboardSubtitle": "Affichage : {totalCount} {totalCount, plural, one {hôte} other {hôtes}}", "xpack.securitySolution.overview.topNLabel": "Premiers {fieldName}", "xpack.securitySolution.pages.common.updateAlertStatusFailed": "Impossible de mettre à jour { conflicts } {conflicts, plural, =1 {alerte} other {alertes}}.", "xpack.securitySolution.pages.common.updateAlertStatusFailedDetailed": "{ updated } {updated, plural, =1 {alerte a été mise à jour} other {alertes ont été mises à jour}} correctement, mais { conflicts } n'ont pas pu être mis à jour\n car { conflicts, plural, =1 {elle était} other {elles étaient}} déjà en cours de modification.", @@ -25537,7 +25531,6 @@ "xpack.securitySolution.responder.header.lastSeen": "Vu en dernier le {date}", "xpack.securitySolution.responder.hostOffline.callout.body": "L'hôte {name} est hors connexion, donc ses réponses peuvent avoir du retard. Les commandes en attente seront exécutées quand l'hôte se reconnectera.", "xpack.securitySolution.responseActionsList.flyout.title": "Log d'action : {hostname}", - "xpack.securitySolution.responseActionsList.list.item.command": "{command}", "xpack.securitySolution.responseActionsList.list.item.hasExpired": "Échec de {command} : action expirée", "xpack.securitySolution.responseActionsList.list.item.hasFailed": "Échec de {command}", "xpack.securitySolution.responseActionsList.list.item.isPending": "{command} est en attente", @@ -25594,7 +25587,6 @@ "xpack.securitySolution.uncommonProcessTable.unit": "{totalCount, plural, other {processus}}", "xpack.securitySolution.useInputHints.exampleInstructions": "Ex : [ {exampleUsage} ]", "xpack.securitySolution.useInputHints.unknownCommand": "Commande inconnue {commandName}", - "xpack.securitySolution.users.userRiskInformation.learnMore": "Pour en savoir plus sur le risque de l'utilisateur, cliquez {usersRiskScoreDocumentationLink}", "xpack.securitySolution.usersRiskTable.filteredUsersTitle": "Afficher les utilisateurs à risque {severity}", "xpack.securitySolution.usersTable.rows": "{numRows} {numRows, plural, =0 {ligne} =1 {ligne} other {lignes}}", "xpack.securitySolution.usersTable.unit": "{totalCount, plural, =1 {utilisateur} other {utilisateurs}}", @@ -25619,7 +25611,6 @@ "xpack.securitySolution.alertDetails.overview.highlightedFields.field": "Champ", "xpack.securitySolution.alertDetails.overview.highlightedFields.value": "Valeur", "xpack.securitySolution.alertDetails.overview.hostRiskDataTitle": "Données de risque de l’hôte", - "xpack.securitySolution.alertDetails.overview.hostsRiskScoreLink": "Score de risque de l’hôte", "xpack.securitySolution.alertDetails.overview.insights": "Informations exploitables", "xpack.securitySolution.alertDetails.overview.insights.related_alerts_by_process_ancestry": "Alertes connexes par processus ancêtre", "xpack.securitySolution.alertDetails.overview.insights.related_alerts_by_process_ancestry_error": "Impossible de récupérer les alertes.", @@ -27096,18 +27087,12 @@ "xpack.securitySolution.detectionEngine.noPermissionsMessage": "Pour afficher les alertes, vous devez mettre à jour les privilèges. Pour en savoir plus, contactez votre administrateur Kibana.", "xpack.securitySolution.detectionEngine.noPermissionsTitle": "Privilèges requis", "xpack.securitySolution.detectionEngine.pageTitle": "Moteur de détection", - "xpack.securitySolution.detectionEngine.previewRule.fieldAdditionalLookBackHelpText": "Ajoute du temps à la période de récupération pour éviter de manquer des alertes.", - "xpack.securitySolution.detectionEngine.previewRule.fieldAdditionalLookBackLabel": "Temps de récupération supplémentaire", - "xpack.securitySolution.detectionEngine.previewRule.fieldIntervalHelpText": "Les règles s'exécutent de façon régulière et détectent les alertes dans la période de temps spécifiée.", - "xpack.securitySolution.detectionEngine.previewRule.fieldIntervalLabel": "S'exécute toutes les (intervalle des règles)", "xpack.securitySolution.detectionEngine.queryPreview.actions": "Actions", "xpack.securitySolution.detectionEngine.queryPreview.histogramDisclaimer": "Remarque : Les alertes ayant plusieurs valeurs event.category seront comptées plusieurs fois.", "xpack.securitySolution.detectionEngine.queryPreview.mlHistogramDisclaimer": "Remarque : Les alertes ayant plusieurs valeurs host.name seront comptées plusieurs fois.", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphCountLabel": "Décompte", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewError": "Erreur de récupération de l'aperçu", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "Avertissement de bruit : cette règle peut générer beaucoup de bruit. Envisagez d'affiner votre recherche. La base est une progression linéaire comportant 1 alerte par heure.", - "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewDisclaimer": "Remarque : cet aperçu exclut les effets d'exceptions aux règles et les remplacements d'horodatages.", - "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText": "Sélectionnez une période de temps pour les données afin d'afficher l'aperçu des résultats de requête.", "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewInvocationCountWarningMessage": "La durée et l'intervalle de règle que vous avez sélectionnés pour la prévisualisation de cette règle peuvent provoquer un dépassement de délai ou prendre beaucoup de temps à s'exécuter. Essayez de réduire la durée et/ou d'augmenter l'intervalle si l'aperçu a expiré (cela n'affectera pas l'exécution de la règle).", "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewInvocationCountWarningTitle": "Le délai d'aperçu des règles peut entraîner un dépassement de délai", "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel": "Durée", @@ -28026,7 +28011,6 @@ "xpack.securitySolution.exceptions.clearExceptionsLabel": "Retirer la liste d'exceptions", "xpack.securitySolution.exceptions.commentEventLabel": "a ajouté un commentaire", "xpack.securitySolution.exceptions.dissasociateExceptionListError": "Impossible de retirer la liste d'exceptions", - "xpack.securitySolution.exceptions.dissasociateListSuccessText": "La liste d'exceptions ({id}) a été retirée avec succès", "xpack.securitySolution.exceptions.editException.bulkCloseLabel": "Fermer toutes les alertes qui correspondent à cette exception et ont été générées par cette règle", "xpack.securitySolution.exceptions.editException.bulkCloseLabel.disabled": "Fermer toutes les alertes qui correspondent à cette exception et ont été générées par cette règle (les listes et les champs non ECS ne sont pas pris en charge)", "xpack.securitySolution.exceptions.editException.cancel": "Annuler", @@ -28040,11 +28024,8 @@ "xpack.securitySolution.exceptions.editException.versionConflictDescription": "Cette exception semble avoir été mise à jour depuis que vous l'avez sélectionnée pour la modifier. Essayez de cliquer sur \"Annuler\" et de modifier à nouveau l'exception.", "xpack.securitySolution.exceptions.editException.versionConflictTitle": "Désolé, une erreur est survenue", "xpack.securitySolution.exceptions.errorLabel": "Erreur", - "xpack.securitySolution.exceptions.failedLoadPolicies": "Une erreur s'est produite lors du chargement des politiques : \"{error}\"", - "xpack.securitySolution.exceptions.fetch404Error": "La liste d'exceptions associée ({listId}) n'existe plus. Veuillez retirer la liste d'exceptions manquante pour ajouter des exceptions supplémentaires à la règle de détection.", "xpack.securitySolution.exceptions.fetchError": "Erreur lors de la récupération de la liste d'exceptions", - "xpack.securitySolution.exceptions.hideCommentsLabel": "Masquer ({comments}) {comments, plural, =1 {commentaire} other {commentaires}}", - "xpack.securitySolution.exceptions.modalErrorAccordionText": "Afficher les informations de référence de la règle :", + "xpack.securitySolution.exceptions.modalErrorAccordionText": "Afficher les informations de référence de la règle :", "xpack.securitySolution.exceptions.exceptionItem.conditions.and": "AND", "xpack.securitySolution.exceptions.exceptionItem.conditions.existsOperator": "existe", "xpack.securitySolution.exceptions.exceptionItem.conditions.existsOperator.not": "n'existe pas", @@ -28066,8 +28047,6 @@ "xpack.securitySolution.exceptions.exceptionItem.editItemButton": "Modifier l’élément", "xpack.securitySolution.exceptions.exceptionItem.metaDetailsBy": "par", "xpack.securitySolution.exceptions.exceptionItem.updatedLabel": "Mis à jour", - "xpack.securitySolution.exceptions.fetchError": "Erreur lors de la récupération de la liste d'exceptions", - "xpack.securitySolution.exceptions.modalErrorAccordionText": "Afficher les informations de référence de la règle :", "xpack.securitySolution.exceptions.operatingSystemFullLabel": "Système d'exploitation", "xpack.securitySolution.exceptions.operatingSystemLinux": "Linux", "xpack.securitySolution.exceptions.operatingSystemMac": "macOS", @@ -28077,7 +28056,6 @@ "xpack.securitySolution.exceptions.referenceModalDeleteButton": "Retirer la liste d'exceptions", "xpack.securitySolution.exceptions.referenceModalTitle": "Retirer la liste d'exceptions", "xpack.securitySolution.exceptions.searchPlaceholder": "par ex. Exemple de liste de noms", - "xpack.securitySolution.exceptions.showCommentsLabel": "Afficher ({comments}) {comments, plural, =1 {commentaire} other {commentaires}}", "xpack.securitySolution.exceptions.viewer.addCommentPlaceholder": "Ajouter un nouveau commentaire...", "xpack.securitySolution.exceptions.viewer.addToClipboard": "Commentaire", "xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel": "Ajouter une exception à une règle", @@ -28442,7 +28420,6 @@ "xpack.securitySolution.navigation.network": "Réseau", "xpack.securitySolution.navigation.newRuleTitle": "Créer une nouvelle règle", "xpack.securitySolution.navigation.overview": "Aperçu", - "xpack.securitySolution.navigation.responseActions": "Actions de réponse", "xpack.securitySolution.navigation.rules": "Règles", "xpack.securitySolution.navigation.threatIntelligence": "Threat Intelligence", "xpack.securitySolution.navigation.timelines": "Chronologies", @@ -28596,7 +28573,6 @@ "xpack.securitySolution.overview.ctiDashboardOtherDatasourceTitle": "Autres", "xpack.securitySolution.overview.ctiDashboardTitle": "Threat Intelligence", "xpack.securitySolution.overview.ctiViewDasboard": "Afficher le tableau de bord", - "xpack.securitySolution.overview.enableRiskScorePopoverTitle": "Les alertes doivent être disponibles avant d'activer le module", "xpack.securitySolution.overview.endgameDnsTitle": "DNS", "xpack.securitySolution.overview.endgameFileTitle": "Fichier", "xpack.securitySolution.overview.endgameImageLoadTitle": "Chargement de la page", @@ -28622,7 +28598,6 @@ "xpack.securitySolution.overview.hostStatGroupFilebeat": "Filebeat", "xpack.securitySolution.overview.hostStatGroupWinlogbeat": "Winlogbeat", "xpack.securitySolution.overview.hostsTitle": "Événements d'hôte", - "xpack.securitySolution.overview.importDasboard": "Importer un tableau de bord", "xpack.securitySolution.overview.landingCards.box.cloudCard.desc": "Évaluez votre niveau de cloud et protégez vos charges de travail contre les attaques.", "xpack.securitySolution.overview.landingCards.box.cloudCard.title": "Protection cloud de bout en bout", "xpack.securitySolution.overview.landingCards.box.endpoint.desc": "Prévention, collecte, détection et réponse, le tout avec Elastic Agent.", @@ -28645,14 +28620,6 @@ "xpack.securitySolution.overview.packetBeatFlowTitle": "Flux", "xpack.securitySolution.overview.packetbeatTLSTitle": "TLS", "xpack.securitySolution.overview.recentTimelinesSidebarTitle": "Chronologies récentes", - "xpack.securitySolution.overview.riskyHostsDashboardDangerPanelButton": "Activer via Dev Tools", - "xpack.securitySolution.overview.riskyHostsDashboardDangerPanelTitle": "Pas de données de score de risque de l'hôte", - "xpack.securitySolution.overview.riskyHostsDashboardEnableThreatIntel": "Vous devez activer le module de risque des hôtes pour visualiser les hôtes à risque.", - "xpack.securitySolution.overview.riskyHostsDashboardLearnMoreButton": "En savoir plus", - "xpack.securitySolution.overview.riskyHostsDashboardTitle": "Scores de risque de l'hôte actuel", - "xpack.securitySolution.overview.riskyHostsDashboardWarningPanelBody": "Nous n'avons détecté aucune donnée de score de risque de l'hôte provenant des hôtes de votre environnement pour la plage temporelle sélectionnée.", - "xpack.securitySolution.overview.riskyHostsDashboardWarningPanelTitle": "Aucune donnée de score de risque de l'hôte disponible pour l'affichage", - "xpack.securitySolution.overview.riskyHostsSource": "Source", "xpack.securitySolution.overview.signalCountTitle": "Tendance des alertes", "xpack.securitySolution.overview.viewAlertsButtonLabel": "Afficher les alertes", "xpack.securitySolution.overview.viewEventsButtonLabel": "Afficher les événements", @@ -28753,7 +28720,6 @@ "xpack.securitySolution.responseActionsList.list.screenReader.expand": "Développer les lignes", "xpack.securitySolution.responseActionsList.list.status": "Statut", "xpack.securitySolution.responseActionsList.list.time": "Heure", - "xpack.securitySolution.responseActionsList.list.title": "Actions de réponse", "xpack.securitySolution.responseActionsList.list.user": "Utilisateur", "xpack.securitySolution.riskScore.errorSearchDescription": "Une erreur s'est produite sur la recherche du score de risque", "xpack.securitySolution.riskScore.failSearchDescription": "Impossible de lancer une recherche sur le score de risque", @@ -31420,7 +31386,6 @@ "xpack.synthetics.uptimeSettings.index": "Paramètres Uptime - Index", "xpack.synthetics.waterfallChart.sidebar.url.https": "https", "xpack.threatIntelligence.common.emptyPage.body3": "Pour vous lancer avec Elastic Threat Intelligence, activez une ou plusieurs intégrations Threat Intelligence depuis la page Intégrations ou bien ingérez des données avec Filebeat. Pour plus d'informations, consultez la ressource {docsLink}.", - "xpack.threatIntelligence.indicator.flyout.panelTitle": "Indicateur : {title}", "xpack.threatIntelligence.common.emptyPage.body1": "Elastic Threat Intelligence facilite l'analyse et l'investigation des menaces potentielles pour la sécurité en regroupant les données de plusieurs sources en un seul endroit.", "xpack.threatIntelligence.common.emptyPage.body2": "Vous pourrez consulter les données de tous les flux Threat Intelligence activés et prendre des mesures à partir de cette page.", "xpack.threatIntelligence.common.emptyPage.buttonText": "Ajouter des intégrations", @@ -31431,18 +31396,16 @@ "xpack.threatIntelligence.empty.title": "Aucun résultat ne correspond à vos critères de recherche.", "xpack.threatIntelligence.indicator.flyout.jsonTabLabel": "JSON", "xpack.threatIntelligence.indicator.flyout.tableTabLabel": "Tableau", - "xpack.threatIntelligence.indicator.flyoutJson.errorMessageBody": "Une erreur s'est produite lors de l'affichage des champs et des valeurs des indicateurs.", - "xpack.threatIntelligence.indicator.flyoutJson.errorMessageTitle": "Impossible d'afficher les informations des indicateurs", "xpack.threatIntelligence.indicator.flyoutTable.errorMessageBody": "Une erreur s'est produite lors de l'affichage des champs et des valeurs des indicateurs.", "xpack.threatIntelligence.indicator.flyoutTable.errorMessageTitle": "Impossible d'afficher les informations des indicateurs", - "xpack.threatIntelligence.indicator.flyoutTable.fieldColumnLabel": "Champ", - "xpack.threatIntelligence.indicator.flyoutTable.valueColumnLabel": "Valeur", + "xpack.threatIntelligence.indicator.fieldsTable.fieldColumnLabel": "Champ", + "xpack.threatIntelligence.indicator.fieldsTable.valueColumnLabel": "Valeur", "xpack.threatIntelligence.indicator.table.actionColumnLabel": "Actions", - "xpack.threatIntelligence.indicator.table.FeedColumTitle": "Fil", - "xpack.threatIntelligence.indicator.table.firstSeenColumTitle": "Vu en premier", - "xpack.threatIntelligence.indicator.table.indicatorColumTitle": "Indicateur", - "xpack.threatIntelligence.indicator.table.indicatorTypeColumTitle": "Type d’indicateur", - "xpack.threatIntelligence.indicator.table.lastSeenColumTitle": "Vu en dernier", + "xpack.threatIntelligence.field.threat.feed.name": "Fil", + "xpack.threatIntelligence.field.threat.indicator.first_seen": "Vu en premier", + "xpack.threatIntelligence.field.threat.indicator.name": "Indicateur", + "xpack.threatIntelligence.field.threat.indicator.type": "Type d’indicateur", + "xpack.threatIntelligence.field.threat.indicator.last_seen": "Vu en dernier", "xpack.threatIntelligence.indicator.table.viewDetailsButton": "Afficher les détails", "xpack.timelines.clipboard.copy.successToastTitle": "Champ {field} copié dans le presse-papiers", "xpack.timelines.footer.autoRefreshActiveTooltip": "Lorsque l'actualisation automatique est activée, la chronologie vous montrera les {numberOfItems} derniers événements correspondant à votre recherche.", @@ -31690,7 +31653,6 @@ "xpack.transform.home.breadcrumbTitle": "Transformations", "xpack.transform.indexPreview.copyClipboardTooltip": "Copier la déclaration Dev Console de l'aperçu de l'index dans le presse-papiers.", "xpack.transform.indexPreview.copyRuntimeFieldsClipboardTooltip": "Copier la déclaration Dev Console des champs de temps d'exécution dans le presse-papiers.", - "xpack.transform.invalidRuntimeFieldMessage": "Champ d'exécution non valide", "xpack.transform.latestPreview.latestPreviewIncompleteConfigCalloutBody": "Veuillez choisir au moins une clé unique et un champ de tri.", "xpack.transform.licenseCheckErrorMessage": "La vérification de la licence a échoué", "xpack.transform.list.emptyPromptButtonText": "Créez votre première transformation", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1d8efe71c1530e..e4a858282956b2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4950,7 +4950,6 @@ "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesFilterPlaceholderText": "変数をフィルター", "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText": "ヘルプ", "unifiedSearch.filter.filterBar.filterItemBadgeIconAriaLabel": "{filter}を削除", - "unifiedSearch.filter.filterBar.labelErrorInfo": "インデックスパターン{indexPattern}が見つかりません", "unifiedSearch.filter.filterBar.labelWarningInfo": "フィールド{fieldName}は現在のビューに存在しません", "unifiedSearch.kueryAutocomplete.andOperatorDescription": "{bothArguments} が true であることを条件とする", "unifiedSearch.kueryAutocomplete.equalOperatorDescription": "一部の値に{equals}", @@ -5023,7 +5022,6 @@ "unifiedSearch.filter.filterBar.includeFilterButtonLabel": "結果を含める", "unifiedSearch.filter.filterBar.indexPatternSelectPlaceholder": "データビューを選択", "unifiedSearch.filter.filterBar.invalidDateFormatProvidedErrorMessage": "無効な日付形式が指定されました", - "unifiedSearch.filter.filterBar.labelErrorText": "エラー", "unifiedSearch.filter.filterBar.labelWarningText": "警告", "unifiedSearch.filter.filterBar.negatedFilterPrefix": "NOT ", "unifiedSearch.filter.filterBar.pinFilterButtonLabel": "すべてのアプリにピン付け", @@ -15025,7 +15023,6 @@ "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshot.repositoryHelpText": "各フェーズは同じスナップショットリポジトリを使用します。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshot.storageHelpText": "検索可能なスナップショットにマウントされたスナップショットのタイプ。これは高度なオプションです。作業内容を理解している場合にのみ変更してください。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshot.storageLabel": "ストレージ", - "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotCalloutBody": "このフェーズでデータを完全にマウントされたインデックスに変換するときには、強制マージ、縮小、読み取り専用アクションは許可されません。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutBody": "検索可能なスナップショットを作成するには、エンタープライズライセンスが必要です。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutTitle": "エンタープライズライセンスが必要です", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoFieldLabel": "スナップショットリポジトリ", @@ -25189,7 +25186,6 @@ "xpack.security.unauthenticated.pageTitle": "ログインできませんでした", "xpack.security.users.breadcrumb": "ユーザー", "xpack.security.users.editUserPage.createBreadcrumb": "作成", - "xpack.securitySolution.alertDetails.overview.hostDataTooltipContent": "リスク分類は、ホストで使用可能なときにのみ表示されます。環境内で{hostsRiskScoreDocumentationLink}が有効であることを確認してください。", "xpack.securitySolution.alertDetails.overview.insights_related_alerts_by_source_event_count": "ソースイベントに関連する{count} {count, plural, other {件のアラート}}", "xpack.securitySolution.alertDetails.overview.insights_related_cases_found_content": "このアラートは{caseCount}で見つかりました", "xpack.securitySolution.alertDetails.overview.insights_related_cases_found_content_count": "{caseCount} {caseCount, plural, other {個のケース:}}", @@ -25443,7 +25439,6 @@ "xpack.securitySolution.hostIsolationExceptions.flyoutCreateSubmitSuccess": "\"{name}\"はホスト分離例外リストに追加されました。", "xpack.securitySolution.hostIsolationExceptions.flyoutEditSubmitSuccess": "\"{name}\"が更新されました。", "xpack.securitySolution.hostIsolationExceptions.showingTotal": "{total} {total, plural, other {個のホスト分離例外}}", - "xpack.securitySolution.hosts.hostRiskInformation.learnMore": "ホストリスクの詳細をご覧ください。{hostsRiskScoreDocumentationLink}", "xpack.securitySolution.hosts.navigaton.eventsUnit": "{totalCount, plural, other {イベント}}", "xpack.securitySolution.hostsRiskTable.filteredHostsTitle": "{severity}のリスクがあるホストを表示", "xpack.securitySolution.hostsTable.rows": "{numRows} {numRows, plural, other {行}}", @@ -25496,7 +25491,6 @@ "xpack.securitySolution.overview.ctiDashboardSubtitle": "{totalCount} {totalCount, plural, other {個の指標}}を表示しています", "xpack.securitySolution.overview.overviewHost.hostsSubtitle": "表示中:{formattedHostEventsCount} {hostEventsCount, plural, other {イベント}}", "xpack.securitySolution.overview.overviewNetwork.networkSubtitle": "表示中:{formattedNetworkEventsCount} {networkEventsCount, plural, other {イベント}}", - "xpack.securitySolution.overview.riskyHostsDashboardSubtitle": "{totalCount} {totalCount, plural, other {個のホスト}}を表示しています", "xpack.securitySolution.overview.topNLabel": "トップ{fieldName}", "xpack.securitySolution.pages.common.updateAlertStatusFailed": "{ conflicts } {conflicts, plural, other {アラート}}を更新できませんでした。", "xpack.securitySolution.pages.common.updateAlertStatusFailedDetailed": "{ updated } {updated, plural, other {アラート}}が正常に更新されましたが、{ conflicts }は更新できませんでした。\n { conflicts, plural, other {}}すでに修正されています。", @@ -25514,7 +25508,6 @@ "xpack.securitySolution.responder.header.lastSeen": "前回表示日時 {date}", "xpack.securitySolution.responder.hostOffline.callout.body": "ホスト{name}はオフラインであるため、応答が遅延する可能性があります。保留中のコマンドは、ホストが再接続されたときに実行されます。", "xpack.securitySolution.responseActionsList.flyout.title": "アクションログ:{hostname}", - "xpack.securitySolution.responseActionsList.list.item.command": "{command}", "xpack.securitySolution.responseActionsList.list.item.hasExpired": "{command}が失敗しました:アクションの有効期限が切れました", "xpack.securitySolution.responseActionsList.list.item.hasFailed": "{command}が失敗しました", "xpack.securitySolution.responseActionsList.list.item.isPending": "{command}は保留中です", @@ -25571,7 +25564,6 @@ "xpack.securitySolution.uncommonProcessTable.unit": "{totalCount, plural, other {プロセス}}", "xpack.securitySolution.useInputHints.exampleInstructions": "例:[ {exampleUsage} ]", "xpack.securitySolution.useInputHints.unknownCommand": "不明なコマンド{commandName}", - "xpack.securitySolution.users.userRiskInformation.learnMore": "ユーザーリスクの詳細をご覧ください。{usersRiskScoreDocumentationLink}", "xpack.securitySolution.usersRiskTable.filteredUsersTitle": "{severity}リスクのユーザーを表示", "xpack.securitySolution.usersTable.rows": "{numRows} {numRows, plural, other {行}}", "xpack.securitySolution.usersTable.unit": "{totalCount, plural, other {ユーザー}}", @@ -25596,7 +25588,6 @@ "xpack.securitySolution.alertDetails.overview.highlightedFields.field": "フィールド", "xpack.securitySolution.alertDetails.overview.highlightedFields.value": "値", "xpack.securitySolution.alertDetails.overview.hostRiskDataTitle": "ホストリスクデータ", - "xpack.securitySolution.alertDetails.overview.hostsRiskScoreLink": "ホストリスクスコア", "xpack.securitySolution.alertDetails.overview.insights": "インサイト", "xpack.securitySolution.alertDetails.overview.insights.related_alerts_by_process_ancestry": "上位プロセス別関連アラート", "xpack.securitySolution.alertDetails.overview.insights.related_alerts_by_process_ancestry_error": "アラートを取得できませんでした。", @@ -27073,18 +27064,12 @@ "xpack.securitySolution.detectionEngine.noPermissionsMessage": "アラートを表示するには、権限を更新する必要があります。詳細については、Kibana管理者に連絡してください。", "xpack.securitySolution.detectionEngine.noPermissionsTitle": "権限が必要です", "xpack.securitySolution.detectionEngine.pageTitle": "検出エンジン", - "xpack.securitySolution.detectionEngine.previewRule.fieldAdditionalLookBackHelpText": "ルックバック期間に時間を追加してアラートの見落としを防ぎます。", - "xpack.securitySolution.detectionEngine.previewRule.fieldAdditionalLookBackLabel": "追加のルックバック時間", - "xpack.securitySolution.detectionEngine.previewRule.fieldIntervalHelpText": "ルールを定期的に実行し、指定の時間枠内でアラートを検出します。", - "xpack.securitySolution.detectionEngine.previewRule.fieldIntervalLabel": "次の間隔で実行(ルール間隔)", "xpack.securitySolution.detectionEngine.queryPreview.actions": "アクション", "xpack.securitySolution.detectionEngine.queryPreview.histogramDisclaimer": "注:複数のevent.category値のアラートは2回以上カウントされます。", "xpack.securitySolution.detectionEngine.queryPreview.mlHistogramDisclaimer": "注:複数のhost.name値のアラートは2回以上カウントされます。", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphCountLabel": "カウント", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewError": "プレビュー取得エラー", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "ノイズ警告:このルールではノイズが多く生じる可能性があります。クエリを絞り込むことを検討してください。これは1時間ごとに1アラートという線形進行に基づいています。", - "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewDisclaimer": "注:このプレビューは、ルール例外とタイムスタンプオーバーライドの効果を除外します。", - "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText": "クエリ結果をプレビューするデータのタイムフレームを選択します。", "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewInvocationCountWarningMessage": "このルールのプレビューで選択したタイムフレームとルール間隔は、タイムアウトになるか、実行に時間がかかる可能性があります。プレビューがタイムアウトする場合は、タイムフレームを短くしたり、間隔を長くしたりしてください(これは実際のルール実行には影響しません)。", "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewInvocationCountWarningTitle": "ルールプレビュータイムフレームはタイムアウトが発生する場合があります", "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel": "タイムフレーム", @@ -28003,7 +27988,6 @@ "xpack.securitySolution.exceptions.clearExceptionsLabel": "例外リストを削除", "xpack.securitySolution.exceptions.commentEventLabel": "コメントを追加しました", "xpack.securitySolution.exceptions.dissasociateExceptionListError": "例外リストを削除できませんでした", - "xpack.securitySolution.exceptions.dissasociateListSuccessText": "例外リスト({id})が正常に削除されました", "xpack.securitySolution.exceptions.editException.bulkCloseLabel": "この例外一致し、このルールによって生成された、すべてのアラートを閉じる", "xpack.securitySolution.exceptions.editException.bulkCloseLabel.disabled": "この例外と一致し、このルールによって生成された、すべてのアラートを閉じる(リストと非ECSフィールドはサポートされません)", "xpack.securitySolution.exceptions.editException.cancel": "キャンセル", @@ -28017,10 +28001,7 @@ "xpack.securitySolution.exceptions.editException.versionConflictDescription": "最初に編集することを選択したときからこの例外が更新されている可能性があります。[キャンセル]をクリックし、もう一度例外を編集してください。", "xpack.securitySolution.exceptions.editException.versionConflictTitle": "申し訳ございません、エラーが発生しました", "xpack.securitySolution.exceptions.errorLabel": "エラー", - "xpack.securitySolution.exceptions.failedLoadPolicies": "ポリシーの読み込みエラーが発生しました:\"{error}\"", - "xpack.securitySolution.exceptions.fetch404Error": "関連付けられた例外リスト({listId})は存在しません。その他の例外を検出ルールに追加するには、見つからない例外リストを削除してください。", "xpack.securitySolution.exceptions.fetchError": "例外リストの取得エラー", - "xpack.securitySolution.exceptions.hideCommentsLabel": "({comments}){comments, plural, other {件のコメント}}を非表示", "xpack.securitySolution.exceptions.exceptionItem.conditions.and": "AND", "xpack.securitySolution.exceptions.exceptionItem.conditions.existsOperator": "存在する", "xpack.securitySolution.exceptions.exceptionItem.conditions.existsOperator.not": "存在しない", @@ -28042,7 +28023,6 @@ "xpack.securitySolution.exceptions.exceptionItem.editItemButton": "項目を編集", "xpack.securitySolution.exceptions.exceptionItem.metaDetailsBy": "グループ基準", "xpack.securitySolution.exceptions.exceptionItem.updatedLabel": "更新しました", - "xpack.securitySolution.exceptions.fetchError": "例外リストの取得エラー", "xpack.securitySolution.exceptions.modalErrorAccordionText": "ルール参照情報を表示:", "xpack.securitySolution.exceptions.operatingSystemFullLabel": "オペレーティングシステム", "xpack.securitySolution.exceptions.operatingSystemLinux": "Linux", @@ -28053,7 +28033,6 @@ "xpack.securitySolution.exceptions.referenceModalDeleteButton": "例外リストを削除", "xpack.securitySolution.exceptions.referenceModalTitle": "例外リストを削除", "xpack.securitySolution.exceptions.searchPlaceholder": "例:例外リスト名", - "xpack.securitySolution.exceptions.showCommentsLabel": "({comments}){comments, plural, other {件のコメント}}を表示", "xpack.securitySolution.exceptions.viewer.addCommentPlaceholder": "新しいコメントを追加...", "xpack.securitySolution.exceptions.viewer.addToClipboard": "コメント", "xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel": "ルール例外の追加", @@ -28418,7 +28397,6 @@ "xpack.securitySolution.navigation.network": "ネットワーク", "xpack.securitySolution.navigation.newRuleTitle": "新規ルールを作成", "xpack.securitySolution.navigation.overview": "概要", - "xpack.securitySolution.navigation.responseActions": "対応アクション", "xpack.securitySolution.navigation.rules": "ルール", "xpack.securitySolution.navigation.threatIntelligence": "脅威インテリジェンス", "xpack.securitySolution.navigation.timelines": "タイムライン", @@ -28572,7 +28550,6 @@ "xpack.securitySolution.overview.ctiDashboardOtherDatasourceTitle": "その他", "xpack.securitySolution.overview.ctiDashboardTitle": "脅威インテリジェンス", "xpack.securitySolution.overview.ctiViewDasboard": "ダッシュボードを表示", - "xpack.securitySolution.overview.enableRiskScorePopoverTitle": "モジュールを有効にする前に、アラートが使用可能でなければなりません", "xpack.securitySolution.overview.endgameDnsTitle": "DNS", "xpack.securitySolution.overview.endgameFileTitle": "ファイル", "xpack.securitySolution.overview.endgameImageLoadTitle": "画像読み込み", @@ -28598,7 +28575,6 @@ "xpack.securitySolution.overview.hostStatGroupFilebeat": "Filebeat", "xpack.securitySolution.overview.hostStatGroupWinlogbeat": "Winlogbeat", "xpack.securitySolution.overview.hostsTitle": "ホストイベント", - "xpack.securitySolution.overview.importDasboard": "ダッシュボードをインポート", "xpack.securitySolution.overview.landingCards.box.cloudCard.desc": "クラウド態勢を評価し、ワークロードを攻撃から保護します。", "xpack.securitySolution.overview.landingCards.box.cloudCard.title": "エンドツーエンドのクラウド保護", "xpack.securitySolution.overview.landingCards.box.endpoint.desc": "防御から収集、検知、対応まで実行する、Elastic Agent。", @@ -28621,14 +28597,6 @@ "xpack.securitySolution.overview.packetBeatFlowTitle": "フロー", "xpack.securitySolution.overview.packetbeatTLSTitle": "TLS", "xpack.securitySolution.overview.recentTimelinesSidebarTitle": "最近のタイムライン", - "xpack.securitySolution.overview.riskyHostsDashboardDangerPanelButton": "開発ツールで有効化", - "xpack.securitySolution.overview.riskyHostsDashboardDangerPanelTitle": "ホストリスクスコアデータがありません", - "xpack.securitySolution.overview.riskyHostsDashboardEnableThreatIntel": "リスクがあるホストを表示するには、ホストリスクモジュールを有効化する必要があります。", - "xpack.securitySolution.overview.riskyHostsDashboardLearnMoreButton": "詳細情報", - "xpack.securitySolution.overview.riskyHostsDashboardTitle": "現在のホストリスクスコア", - "xpack.securitySolution.overview.riskyHostsDashboardWarningPanelBody": "選択した期間では、ご使用の環境のホストからホストリスクスコアデータが検出されませんでした。", - "xpack.securitySolution.overview.riskyHostsDashboardWarningPanelTitle": "表示するホストリスクスコアデータがありません", - "xpack.securitySolution.overview.riskyHostsSource": "送信元", "xpack.securitySolution.overview.signalCountTitle": "アラート傾向", "xpack.securitySolution.overview.viewAlertsButtonLabel": "アラートを表示", "xpack.securitySolution.overview.viewEventsButtonLabel": "イベントを表示", @@ -28729,7 +28697,6 @@ "xpack.securitySolution.responseActionsList.list.screenReader.expand": "行を展開", "xpack.securitySolution.responseActionsList.list.status": "ステータス", "xpack.securitySolution.responseActionsList.list.time": "時間", - "xpack.securitySolution.responseActionsList.list.title": "対応アクション", "xpack.securitySolution.responseActionsList.list.user": "ユーザー", "xpack.securitySolution.riskScore.errorSearchDescription": "リスクスコア検索でエラーが発生しました", "xpack.securitySolution.riskScore.failSearchDescription": "リスクスコアで検索を実行できませんでした", @@ -31397,7 +31364,6 @@ "xpack.synthetics.uptimeSettings.index": "アップタイム設定 - インデックス", "xpack.synthetics.waterfallChart.sidebar.url.https": "https", "xpack.threatIntelligence.common.emptyPage.body3": "Elastic Threat Intelligenceを開始するには、[統合]ページから1つ以上の脅威インテリジェンス統合を有効にするか、Filebeatを使用してデータを取り込みます。詳細については、{docsLink}をご覧ください。", - "xpack.threatIntelligence.indicator.flyout.panelTitle": "インジケーター:{title}", "xpack.threatIntelligence.common.emptyPage.body1": "Elastic Threat Intelligenceでは、複数のソースのデータを一元的に集約することで、潜在的なセキュリティ脅威を簡単に分析、調査できます。", "xpack.threatIntelligence.common.emptyPage.body2": "すべてのアクティブな脅威インテリジェンスフィードのデータを表示し、このページからアクションを実行できます。", "xpack.threatIntelligence.common.emptyPage.buttonText": "統合の追加", @@ -31408,18 +31374,16 @@ "xpack.threatIntelligence.empty.title": "検索条件と一致する結果がありません。", "xpack.threatIntelligence.indicator.flyout.jsonTabLabel": "JSON", "xpack.threatIntelligence.indicator.flyout.tableTabLabel": "表", - "xpack.threatIntelligence.indicator.flyoutJson.errorMessageBody": "インジケーターフィールドと値の表示エラーが発生しました。", - "xpack.threatIntelligence.indicator.flyoutJson.errorMessageTitle": "インジケーター情報を表示できませn", "xpack.threatIntelligence.indicator.flyoutTable.errorMessageBody": "インジケーターフィールドと値の表示エラーが発生しました。", "xpack.threatIntelligence.indicator.flyoutTable.errorMessageTitle": "インジケーター情報を表示できませn", - "xpack.threatIntelligence.indicator.flyoutTable.fieldColumnLabel": "フィールド", - "xpack.threatIntelligence.indicator.flyoutTable.valueColumnLabel": "値", + "xpack.threatIntelligence.indicator.fieldsTable.fieldColumnLabel": "フィールド", + "xpack.threatIntelligence.indicator.fieldsTable.valueColumnLabel": "値", "xpack.threatIntelligence.indicator.table.actionColumnLabel": "アクション", - "xpack.threatIntelligence.indicator.table.FeedColumTitle": "フィード", - "xpack.threatIntelligence.indicator.table.firstSeenColumTitle": "初回の認識", - "xpack.threatIntelligence.indicator.table.indicatorColumTitle": "インジケーター", - "xpack.threatIntelligence.indicator.table.indicatorTypeColumTitle": "インジケータータイプ", - "xpack.threatIntelligence.indicator.table.lastSeenColumTitle": "前回の認識", + "xpack.threatIntelligence.field.threat.feed.name": "フィード", + "xpack.threatIntelligence.field.threat.indicator.first_seen": "初回の認識", + "xpack.threatIntelligence.field.threat.indicator.name": "インジケーター", + "xpack.threatIntelligence.field.threat.indicator.type": "インジケータータイプ", + "xpack.threatIntelligence.field.threat.indicator.last_seen": "前回の認識", "xpack.threatIntelligence.indicator.table.viewDetailsButton": "詳細を表示", "xpack.timelines.clipboard.copy.successToastTitle": "フィールド{field}をクリップボードにコピーしました", "xpack.timelines.footer.autoRefreshActiveTooltip": "自動更新が有効な間、タイムラインはクエリに一致する最新の {numberOfItems} 件のイベントを表示します。", @@ -31666,7 +31630,6 @@ "xpack.transform.home.breadcrumbTitle": "変換", "xpack.transform.indexPreview.copyClipboardTooltip": "インデックスプレビューの開発コンソールステートメントをクリップボードにコピーします。", "xpack.transform.indexPreview.copyRuntimeFieldsClipboardTooltip": "ランタイムフィールドの開発コンソールステートメントをクリップボードにコピーします。", - "xpack.transform.invalidRuntimeFieldMessage": "無効なランタイムフィールド", "xpack.transform.latestPreview.latestPreviewIncompleteConfigCalloutBody": "1 つ以上の一意キーと並べ替えフィールドを選択してください。", "xpack.transform.licenseCheckErrorMessage": "ライセンス確認失敗", "xpack.transform.list.emptyPromptButtonText": "初めての変換を作成してみましょう。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 120a031c7c9121..7db59a8d229a92 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4956,7 +4956,6 @@ "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesFilterPlaceholderText": "筛选变量", "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText": "帮助", "unifiedSearch.filter.filterBar.filterItemBadgeIconAriaLabel": "删除 {filter}", - "unifiedSearch.filter.filterBar.labelErrorInfo": "找不到索引模式 {indexPattern}", "unifiedSearch.filter.filterBar.labelWarningInfo": "当前视图中不存在字段 {fieldName}", "unifiedSearch.kueryAutocomplete.andOperatorDescription": "需要{bothArguments}为 true", "unifiedSearch.kueryAutocomplete.equalOperatorDescription": "{equals}某一值", @@ -5029,7 +5028,6 @@ "unifiedSearch.filter.filterBar.includeFilterButtonLabel": "包括结果", "unifiedSearch.filter.filterBar.indexPatternSelectPlaceholder": "选择数据视图", "unifiedSearch.filter.filterBar.invalidDateFormatProvidedErrorMessage": "提供的日期格式无效", - "unifiedSearch.filter.filterBar.labelErrorText": "错误", "unifiedSearch.filter.filterBar.labelWarningText": "警告", "unifiedSearch.filter.filterBar.negatedFilterPrefix": "非 ", "unifiedSearch.filter.filterBar.pinFilterButtonLabel": "在所有应用上固定", @@ -15042,7 +15040,6 @@ "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshot.repositoryHelpText": "每个阶段使用相同的快照存储库。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshot.storageHelpText": "为可搜索快照安装的快照类型。这是高级选项。只有了解此功能时才能更改。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshot.storageLabel": "存储", - "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotCalloutBody": "在此阶段将数据转换为完全安装的索引时,不允许强制合并、缩小和只读操作。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutBody": "要创建可搜索快照,需要企业许可证。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutTitle": "需要企业许可证", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoFieldLabel": "快照存储库", @@ -25218,7 +25215,6 @@ "xpack.security.unauthenticated.pageTitle": "我们无法使您登录", "xpack.security.users.breadcrumb": "用户", "xpack.security.users.editUserPage.createBreadcrumb": "创建", - "xpack.securitySolution.alertDetails.overview.hostDataTooltipContent": "仅在其对主机可用时才会显示风险分类。确保在您的环境中启用了 {hostsRiskScoreDocumentationLink}。", "xpack.securitySolution.alertDetails.overview.insights_related_alerts_by_source_event_count": "{count} 个{count, plural, other {告警}}与源事件相关", "xpack.securitySolution.alertDetails.overview.insights_related_cases_found_content": "发现此告警位于 {caseCount}", "xpack.securitySolution.alertDetails.overview.insights_related_cases_found_content_count": "{caseCount} 个{caseCount, plural, other {案例:}}", @@ -25474,7 +25470,6 @@ "xpack.securitySolution.hostIsolationExceptions.flyoutCreateSubmitSuccess": "已将“{name}”添加到您的主机隔离例外列表。", "xpack.securitySolution.hostIsolationExceptions.flyoutEditSubmitSuccess": "“{name}”已更新。", "xpack.securitySolution.hostIsolationExceptions.showingTotal": "正在显示 {total} 个{total, plural, other {主机隔离例外}}", - "xpack.securitySolution.hosts.hostRiskInformation.learnMore": "您可以详细了解主机风险{hostsRiskScoreDocumentationLink}", "xpack.securitySolution.hosts.navigaton.eventsUnit": "{totalCount, plural, other {个事件}}", "xpack.securitySolution.hostsRiskTable.filteredHostsTitle": "查看{severity}风险主机", "xpack.securitySolution.hostsTable.rows": "{numRows} {numRows, plural, other {行}}", @@ -25527,7 +25522,6 @@ "xpack.securitySolution.overview.ctiDashboardSubtitle": "正在显示:{totalCount} 个{totalCount, plural, other {指标}}", "xpack.securitySolution.overview.overviewHost.hostsSubtitle": "正在显示:{formattedHostEventsCount} 个{hostEventsCount, plural, other {事件}}", "xpack.securitySolution.overview.overviewNetwork.networkSubtitle": "正在显示:{formattedNetworkEventsCount} 个{networkEventsCount, plural, other {事件}}", - "xpack.securitySolution.overview.riskyHostsDashboardSubtitle": "正在显示:{totalCount} 台{totalCount, plural, other {主机}}", "xpack.securitySolution.overview.topNLabel": "排名靠前的{fieldName}", "xpack.securitySolution.pages.common.updateAlertStatusFailed": "无法更新{ conflicts } 个{conflicts, plural, other {告警}}。", "xpack.securitySolution.pages.common.updateAlertStatusFailedDetailed": "{ updated } 个{updated, plural, other {告警}}已成功更新,但是 { conflicts } 个无法更新,\n 因为{ conflicts, plural, other {其}}已被修改。", @@ -25545,7 +25539,6 @@ "xpack.securitySolution.responder.header.lastSeen": "最后看到时间 {date}", "xpack.securitySolution.responder.hostOffline.callout.body": "主机 {name} 脱机,因此其响应可能会延迟。主机重新建立连接后将执行待处理的命令。", "xpack.securitySolution.responseActionsList.flyout.title": "操作日志:{hostname}", - "xpack.securitySolution.responseActionsList.list.item.command": "{command}", "xpack.securitySolution.responseActionsList.list.item.hasExpired": "{command} 失败:操作已过期", "xpack.securitySolution.responseActionsList.list.item.hasFailed": "{command} 失败", "xpack.securitySolution.responseActionsList.list.item.isPending": "{command} 待处理", @@ -25602,7 +25595,6 @@ "xpack.securitySolution.uncommonProcessTable.unit": "{totalCount, plural, other {个进程}}", "xpack.securitySolution.useInputHints.exampleInstructions": "例如:[ {exampleUsage} ]", "xpack.securitySolution.useInputHints.unknownCommand": "未知命令 {commandName}", - "xpack.securitySolution.users.userRiskInformation.learnMore": "您可以详细了解用户风险{usersRiskScoreDocumentationLink}", "xpack.securitySolution.usersRiskTable.filteredUsersTitle": "查看{severity}风险用户", "xpack.securitySolution.usersTable.rows": "{numRows} {numRows, plural, other {行}}", "xpack.securitySolution.usersTable.unit": "{totalCount, plural, other {个用户}}", @@ -25627,7 +25619,6 @@ "xpack.securitySolution.alertDetails.overview.highlightedFields.field": "字段", "xpack.securitySolution.alertDetails.overview.highlightedFields.value": "值", "xpack.securitySolution.alertDetails.overview.hostRiskDataTitle": "主机风险数据", - "xpack.securitySolution.alertDetails.overview.hostsRiskScoreLink": "主机风险分数", "xpack.securitySolution.alertDetails.overview.insights": "洞见", "xpack.securitySolution.alertDetails.overview.insights.related_alerts_by_process_ancestry": "按进程体系列出相关告警", "xpack.securitySolution.alertDetails.overview.insights.related_alerts_by_process_ancestry_error": "无法获取告警。", @@ -27104,18 +27095,12 @@ "xpack.securitySolution.detectionEngine.noPermissionsMessage": "要查看告警,必须更新权限。有关详细信息,请联系您的 Kibana 管理员。", "xpack.securitySolution.detectionEngine.noPermissionsTitle": "需要权限", "xpack.securitySolution.detectionEngine.pageTitle": "检测引擎", - "xpack.securitySolution.detectionEngine.previewRule.fieldAdditionalLookBackHelpText": "增加回查时段的时间以防止错过告警。", - "xpack.securitySolution.detectionEngine.previewRule.fieldAdditionalLookBackLabel": "更多回查时间", - "xpack.securitySolution.detectionEngine.previewRule.fieldIntervalHelpText": "规则定期运行并检测指定时间范围内的告警。", - "xpack.securitySolution.detectionEngine.previewRule.fieldIntervalLabel": "运行间隔(规则时间间隔)", "xpack.securitySolution.detectionEngine.queryPreview.actions": "操作", "xpack.securitySolution.detectionEngine.queryPreview.histogramDisclaimer": "注意:具有多个 event.category 值的告警会计算多次。", "xpack.securitySolution.detectionEngine.queryPreview.mlHistogramDisclaimer": "注意:具有多个 host.name 值的告警会计算多次。", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphCountLabel": "计数", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewError": "提取预览时出错", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "噪音警告:此规则可能会导致大量噪音。考虑缩小您的查询范围。这基于每小时 1 条告警的线性级数。", - "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewDisclaimer": "注意:此预览不包括规则例外和时间戳覆盖的影响。", - "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText": "选择数据的时间范围以预览查询结果。", "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewInvocationCountWarningMessage": "您为预览此规则选择的时间范围和规则时间间隔可能会导致超时或需要很长时间才能完成执行。如果预览超时,请尝试减少时间范围和/或增加时间间隔(这不会影响实际规则运行)。", "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewInvocationCountWarningTitle": "规则预览时间范围可能会导致超时", "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel": "时间范围", @@ -28034,7 +28019,6 @@ "xpack.securitySolution.exceptions.clearExceptionsLabel": "移除例外列表", "xpack.securitySolution.exceptions.commentEventLabel": "已添加注释", "xpack.securitySolution.exceptions.dissasociateExceptionListError": "无法移除例外列表", - "xpack.securitySolution.exceptions.dissasociateListSuccessText": "例外列表 ({id}) 已成功移除", "xpack.securitySolution.exceptions.editException.bulkCloseLabel": "关闭所有与此例外匹配且根据此规则生成的告警", "xpack.securitySolution.exceptions.editException.bulkCloseLabel.disabled": "关闭所有与此例外匹配且根据此规则生成的告警(不支持列表和非 ECS 字段)", "xpack.securitySolution.exceptions.editException.cancel": "取消", @@ -28048,10 +28032,7 @@ "xpack.securitySolution.exceptions.editException.versionConflictDescription": "此例外可能自您首次选择编辑后已更新。尝试单击“取消”,重新编辑该例外。", "xpack.securitySolution.exceptions.editException.versionConflictTitle": "抱歉,有错误", "xpack.securitySolution.exceptions.errorLabel": "错误", - "xpack.securitySolution.exceptions.failedLoadPolicies": "加载策略时出错:“{error}”", - "xpack.securitySolution.exceptions.fetch404Error": "关联的例外列表 ({listId}) 已不存在。请移除缺少的例外列表,以将其他例外添加到检测规则。", "xpack.securitySolution.exceptions.fetchError": "提取例外列表时出错", - "xpack.securitySolution.exceptions.hideCommentsLabel": "隐藏 ({comments}) 个{comments, plural, other {注释}}", "xpack.securitySolution.exceptions.exceptionItem.conditions.and": "且", "xpack.securitySolution.exceptions.exceptionItem.conditions.existsOperator": "存在", "xpack.securitySolution.exceptions.exceptionItem.conditions.existsOperator.not": "不存在", @@ -28073,7 +28054,6 @@ "xpack.securitySolution.exceptions.exceptionItem.editItemButton": "编辑项目", "xpack.securitySolution.exceptions.exceptionItem.metaDetailsBy": "依据", "xpack.securitySolution.exceptions.exceptionItem.updatedLabel": "已更新", - "xpack.securitySolution.exceptions.fetchError": "提取例外列表时出错", "xpack.securitySolution.exceptions.modalErrorAccordionText": "显示规则引用信息:", "xpack.securitySolution.exceptions.operatingSystemFullLabel": "操作系统", "xpack.securitySolution.exceptions.operatingSystemLinux": "Linux", @@ -28084,7 +28064,6 @@ "xpack.securitySolution.exceptions.referenceModalDeleteButton": "移除例外列表", "xpack.securitySolution.exceptions.referenceModalTitle": "移除例外列表", "xpack.securitySolution.exceptions.searchPlaceholder": "例如,示例列表名称", - "xpack.securitySolution.exceptions.showCommentsLabel": "显示 ({comments} 个) {comments, plural, other {注释}}", "xpack.securitySolution.exceptions.viewer.addCommentPlaceholder": "添加新注释......", "xpack.securitySolution.exceptions.viewer.addToClipboard": "注释", "xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel": "添加规则例外", @@ -28449,7 +28428,6 @@ "xpack.securitySolution.navigation.network": "网络", "xpack.securitySolution.navigation.newRuleTitle": "创建新规则", "xpack.securitySolution.navigation.overview": "概览", - "xpack.securitySolution.navigation.responseActions": "响应操作", "xpack.securitySolution.navigation.rules": "规则", "xpack.securitySolution.navigation.threatIntelligence": "威胁情报", "xpack.securitySolution.navigation.timelines": "时间线", @@ -28603,7 +28581,6 @@ "xpack.securitySolution.overview.ctiDashboardOtherDatasourceTitle": "其他", "xpack.securitySolution.overview.ctiDashboardTitle": "威胁情报", "xpack.securitySolution.overview.ctiViewDasboard": "查看仪表板", - "xpack.securitySolution.overview.enableRiskScorePopoverTitle": "启用模块之前,告警需要处于可用状态", "xpack.securitySolution.overview.endgameDnsTitle": "DNS", "xpack.securitySolution.overview.endgameFileTitle": "文件", "xpack.securitySolution.overview.endgameImageLoadTitle": "映像加载", @@ -28629,7 +28606,6 @@ "xpack.securitySolution.overview.hostStatGroupFilebeat": "Filebeat", "xpack.securitySolution.overview.hostStatGroupWinlogbeat": "Winlogbeat", "xpack.securitySolution.overview.hostsTitle": "主机事件", - "xpack.securitySolution.overview.importDasboard": "导入仪表板", "xpack.securitySolution.overview.landingCards.box.cloudCard.desc": "评估您的云态势并防止工作负载受到攻击。", "xpack.securitySolution.overview.landingCards.box.cloudCard.title": "端到端云防护", "xpack.securitySolution.overview.landingCards.box.endpoint.desc": "防御、收集、检测和响应 — 所有这些活动均可通过 Elastic 代理来实现。", @@ -28652,14 +28628,6 @@ "xpack.securitySolution.overview.packetBeatFlowTitle": "流", "xpack.securitySolution.overview.packetbeatTLSTitle": "TLS", "xpack.securitySolution.overview.recentTimelinesSidebarTitle": "最近的时间线", - "xpack.securitySolution.overview.riskyHostsDashboardDangerPanelButton": "通过开发工具启用", - "xpack.securitySolution.overview.riskyHostsDashboardDangerPanelTitle": "无主机风险分数数据", - "xpack.securitySolution.overview.riskyHostsDashboardEnableThreatIntel": "必须启用主机风险模块才能查看有风险主机。", - "xpack.securitySolution.overview.riskyHostsDashboardLearnMoreButton": "了解详情", - "xpack.securitySolution.overview.riskyHostsDashboardTitle": "当前主机风险分数", - "xpack.securitySolution.overview.riskyHostsDashboardWarningPanelBody": "对于选定时间范围,我们尚未从您环境中的主机中检测到任何主机风险分数数据。", - "xpack.securitySolution.overview.riskyHostsDashboardWarningPanelTitle": "没有可显示的主机风险分数数据", - "xpack.securitySolution.overview.riskyHostsSource": "源", "xpack.securitySolution.overview.signalCountTitle": "告警趋势", "xpack.securitySolution.overview.viewAlertsButtonLabel": "查看告警", "xpack.securitySolution.overview.viewEventsButtonLabel": "查看事件", @@ -28760,7 +28728,6 @@ "xpack.securitySolution.responseActionsList.list.screenReader.expand": "展开行", "xpack.securitySolution.responseActionsList.list.status": "状态", "xpack.securitySolution.responseActionsList.list.time": "时间", - "xpack.securitySolution.responseActionsList.list.title": "响应操作", "xpack.securitySolution.responseActionsList.list.user": "用户", "xpack.securitySolution.riskScore.errorSearchDescription": "搜索风险分数时发生错误", "xpack.securitySolution.riskScore.failSearchDescription": "无法对风险分数执行搜索", @@ -31429,7 +31396,6 @@ "xpack.synthetics.uptimeSettings.index": "Uptime 设置 - 索引", "xpack.synthetics.waterfallChart.sidebar.url.https": "https", "xpack.threatIntelligence.common.emptyPage.body3": "要开始使用 Elastic 威胁情报,请从“集成”页面启用一个或多个威胁情报集成,或使用 Filebeat 采集数据。有关更多信息,请查看 {docsLink}。", - "xpack.threatIntelligence.indicator.flyout.panelTitle": "指标:{title}", "xpack.threatIntelligence.common.emptyPage.body1": "利用 Elastic 威胁情报,可以通过将多个来源的数据聚合到一个位置,轻松分析和调查潜在的安全威胁。", "xpack.threatIntelligence.common.emptyPage.body2": "您将可以查看来自所有已激活威胁情报馈送的数据,并从此页面执行操作。", "xpack.threatIntelligence.common.emptyPage.buttonText": "添加集成", @@ -31440,18 +31406,16 @@ "xpack.threatIntelligence.empty.title": "没有任何结果匹配您的搜索条件", "xpack.threatIntelligence.indicator.flyout.jsonTabLabel": "JSON", "xpack.threatIntelligence.indicator.flyout.tableTabLabel": "表", - "xpack.threatIntelligence.indicator.flyoutJson.errorMessageBody": "显示指标字段和值时出现错误。", - "xpack.threatIntelligence.indicator.flyoutJson.errorMessageTitle": "无法显示指标信息", "xpack.threatIntelligence.indicator.flyoutTable.errorMessageBody": "显示指标字段和值时出现错误。", "xpack.threatIntelligence.indicator.flyoutTable.errorMessageTitle": "无法显示指标信息", - "xpack.threatIntelligence.indicator.flyoutTable.fieldColumnLabel": "字段", - "xpack.threatIntelligence.indicator.flyoutTable.valueColumnLabel": "值", + "xpack.threatIntelligence.indicator.fieldsTable.fieldColumnLabel": "字段", + "xpack.threatIntelligence.indicator.fieldsTable.valueColumnLabel": "值", "xpack.threatIntelligence.indicator.table.actionColumnLabel": "操作", - "xpack.threatIntelligence.indicator.table.FeedColumTitle": "馈送", - "xpack.threatIntelligence.indicator.table.firstSeenColumTitle": "首次看到时间", - "xpack.threatIntelligence.indicator.table.indicatorColumTitle": "指标", - "xpack.threatIntelligence.indicator.table.indicatorTypeColumTitle": "指标类型", - "xpack.threatIntelligence.indicator.table.lastSeenColumTitle": "最后看到时间", + "xpack.threatIntelligence.field.threat.feed.name": "馈送", + "xpack.threatIntelligence.field.threat.indicator.first_seen": "首次看到时间", + "xpack.threatIntelligence.field.threat.indicator.name": "指标", + "xpack.threatIntelligence.field.threat.indicator.type": "指标类型", + "xpack.threatIntelligence.field.threat.indicator.last_seen": "最后看到时间", "xpack.threatIntelligence.indicator.table.viewDetailsButton": "查看详情", "xpack.timelines.clipboard.copy.successToastTitle": "已将字段 {field} 复制到剪贴板", "xpack.timelines.footer.autoRefreshActiveTooltip": "自动刷新已启用时,时间线将显示匹配查询的最近 {numberOfItems} 个事件。", @@ -31699,7 +31663,6 @@ "xpack.transform.home.breadcrumbTitle": "转换", "xpack.transform.indexPreview.copyClipboardTooltip": "将索引预览的开发控制台语句复制到剪贴板。", "xpack.transform.indexPreview.copyRuntimeFieldsClipboardTooltip": "将运行时字段的开发控制台语句复制到剪贴板。", - "xpack.transform.invalidRuntimeFieldMessage": "运行时字段无效", "xpack.transform.latestPreview.latestPreviewIncompleteConfigCalloutBody": "请选择至少一个唯一键和排序字段。", "xpack.transform.licenseCheckErrorMessage": "许可证检查失败", "xpack.transform.list.emptyPromptButtonText": "创建您的首个转换", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_alerts_aggregations.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_alerts_aggregations.test.ts new file mode 100644 index 00000000000000..293c1e992ac18e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_alerts_aggregations.test.ts @@ -0,0 +1,67 @@ +/* + * 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 { ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common'; +import { renderHook } from '@testing-library/react-hooks'; +import { useKibana } from '../../common/lib/kibana'; +import { mockAggsResponse, mockChartData } from '../mock/rule_details/alert_summary'; +import { useLoadRuleAlertsAggs } from './use_load_rule_alerts_aggregations'; + +jest.mock('../../common/lib/kibana'); + +const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>; +describe('useLoadRuleAlertsAggs', () => { + beforeEach(() => { + jest.clearAllMocks(); + useKibanaMock().services.http.post = jest.fn().mockResolvedValue({ ...mockAggsResponse() }); + useKibanaMock().services.http.get = jest.fn().mockResolvedValue({ index_name: ['mock_index'] }); + }); + + it('should return the expected chart data from the Elasticsearch Aggs. query', async () => { + const { result, waitForNextUpdate } = renderHook(() => + useLoadRuleAlertsAggs({ + features: ALERTS_FEATURE_ID, + ruleId: 'c95bc120-1d56-11ed-9cc7-e7214ada1128', + }) + ); + expect(result.current).toEqual({ + isLoadingRuleAlertsAggs: true, + ruleAlertsAggs: { active: 0, recovered: 0 }, + alertsChartData: [], + }); + + await waitForNextUpdate(); + const { ruleAlertsAggs, errorRuleAlertsAggs, alertsChartData } = result.current; + expect(ruleAlertsAggs).toEqual({ + active: 1, + recovered: 7, + }); + expect(alertsChartData).toEqual(mockChartData()); + expect(errorRuleAlertsAggs).toBeFalsy(); + expect(alertsChartData.length).toEqual(33); + }); + + it('should have the correct query body sent to Elasticsearch', async () => { + const ruleId = 'c95bc120-1d56-11ed-9cc7-e7214ada1128'; + const { waitForNextUpdate } = renderHook(() => + useLoadRuleAlertsAggs({ + features: ALERTS_FEATURE_ID, + ruleId, + }) + ); + + await waitForNextUpdate(); + const body = `{"index":"mock_index","size":0,"query":{"bool":{"must":[{"term":{"kibana.alert.rule.uuid":"${ruleId}"}},{"range":{"@timestamp":{"gte":"now-30d","lt":"now"}}},{"bool":{"should":[{"term":{"kibana.alert.status":"active"}},{"term":{"kibana.alert.status":"recovered"}}]}}]}},"aggs":{"total":{"filters":{"filters":{"totalActiveAlerts":{"term":{"kibana.alert.status":"active"}},"totalRecoveredAlerts":{"term":{"kibana.alert.status":"recovered"}}}}},"statusPerDay":{"date_histogram":{"field":"@timestamp","fixed_interval":"1d","extended_bounds":{"min":"now-30d","max":"now"}},"aggs":{"alertStatus":{"terms":{"field":"kibana.alert.status"}}}}}}`; + + expect(useKibanaMock().services.http.post).toHaveBeenCalledWith( + '/internal/rac/alerts/find', + expect.objectContaining({ + body, + }) + ); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_alerts_aggregations.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_alerts_aggregations.ts index aeba9605cb9a6a..c938b0b2cc13f6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_alerts_aggregations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_alerts_aggregations.ts @@ -78,7 +78,7 @@ export function useLoadRuleAlertsAggs({ features, ruleId }: UseLoadRuleAlertsAgg setRuleAlertsAggs((oldState: LoadRuleAlertsAggs) => ({ ...oldState, isLoadingRuleAlertsAggs: false, - errorRuleAlertsAggs: 'error', + errorRuleAlertsAggs: error, alertsChartData: [], })); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/mock/rule_details/alert_summary/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/mock/rule_details/alert_summary/index.ts new file mode 100644 index 00000000000000..b0e5416bbf63db --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/mock/rule_details/alert_summary/index.ts @@ -0,0 +1,445 @@ +/* + * 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 { Rule } from '../../../../types'; +import { AlertChartData } from '../../../sections/rule_details/components/alert_summary'; + +export const mockRule = (): Rule => { + return { + id: '1', + name: 'test rule', + tags: ['tag1'], + enabled: true, + ruleTypeId: 'test_rule_type', + schedule: { interval: '1s' }, + actions: [], + params: { name: 'test rule type name' }, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + createdAt: new Date(), + updatedAt: new Date(), + consumer: 'alerts', + notifyWhen: 'onActiveAlert', + executionStatus: { + status: 'active', + lastDuration: 500, + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, + monitoring: { + execution: { + history: [ + { + success: true, + duration: 1000000, + timestamp: 1234567, + }, + { + success: true, + duration: 200000, + timestamp: 1234567, + }, + { + success: false, + duration: 300000, + timestamp: 1234567, + }, + ], + calculated_metrics: { + success_ratio: 0.66, + p50: 200000, + p95: 300000, + p99: 300000, + }, + }, + }, + }; +}; + +export function mockChartData(): AlertChartData[] { + return [ + { + date: 1660608000000, + count: 6, + status: 'recovered', + }, + { + date: 1660694400000, + count: 1, + status: 'recovered', + }, + { + date: 1660694400000, + count: 1, + status: 'active', + }, + { + date: 1658102400000, + count: 6, + status: 'total', + }, + { + date: 1658188800000, + count: 6, + status: 'total', + }, + { + date: 1658275200000, + count: 6, + status: 'total', + }, + { + date: 1658361600000, + count: 6, + status: 'total', + }, + { + date: 1658448000000, + count: 6, + status: 'total', + }, + { + date: 1658534400000, + count: 6, + status: 'total', + }, + { + date: 1658620800000, + count: 6, + status: 'total', + }, + { + date: 1658707200000, + count: 6, + status: 'total', + }, + { + date: 1658793600000, + count: 6, + status: 'total', + }, + { + date: 1658880000000, + count: 6, + status: 'total', + }, + { + date: 1658966400000, + count: 6, + status: 'total', + }, + { + date: 1659052800000, + count: 6, + status: 'total', + }, + { + date: 1659139200000, + count: 6, + status: 'total', + }, + { + date: 1659225600000, + count: 6, + status: 'total', + }, + { + date: 1659312000000, + count: 6, + status: 'total', + }, + { + date: 1659398400000, + count: 6, + status: 'total', + }, + { + date: 1659484800000, + count: 6, + status: 'total', + }, + { + date: 1659571200000, + count: 6, + status: 'total', + }, + { + date: 1659657600000, + count: 6, + status: 'total', + }, + { + date: 1659744000000, + count: 6, + status: 'total', + }, + { + date: 1659830400000, + count: 6, + status: 'total', + }, + { + date: 1659916800000, + count: 6, + status: 'total', + }, + { + date: 1660003200000, + count: 6, + status: 'total', + }, + { + date: 1660089600000, + count: 6, + status: 'total', + }, + { + date: 1660176000000, + count: 6, + status: 'total', + }, + { + date: 1660262400000, + count: 6, + status: 'total', + }, + { + date: 1660348800000, + count: 6, + status: 'total', + }, + { + date: 1660435200000, + count: 6, + status: 'total', + }, + { + date: 1660521600000, + count: 6, + status: 'total', + }, + { + date: 1660694400000, + count: 4, + status: 'total', + }, + ]; +} + +export const mockAggsResponse = () => { + return { + aggregations: { + total: { + buckets: { totalActiveAlerts: { doc_count: 1 }, totalRecoveredAlerts: { doc_count: 7 } }, + }, + statusPerDay: { + buckets: [ + { + key_as_string: '2022-07-18T00:00:00.000Z', + key: 1658102400000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-07-19T00:00:00.000Z', + key: 1658188800000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-07-20T00:00:00.000Z', + key: 1658275200000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-07-21T00:00:00.000Z', + key: 1658361600000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-07-22T00:00:00.000Z', + key: 1658448000000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-07-23T00:00:00.000Z', + key: 1658534400000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-07-24T00:00:00.000Z', + key: 1658620800000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-07-25T00:00:00.000Z', + key: 1658707200000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-07-26T00:00:00.000Z', + key: 1658793600000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-07-27T00:00:00.000Z', + key: 1658880000000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-07-28T00:00:00.000Z', + key: 1658966400000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-07-29T00:00:00.000Z', + key: 1659052800000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-07-30T00:00:00.000Z', + key: 1659139200000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-07-31T00:00:00.000Z', + key: 1659225600000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-08-01T00:00:00.000Z', + key: 1659312000000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-08-02T00:00:00.000Z', + key: 1659398400000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-08-03T00:00:00.000Z', + key: 1659484800000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-08-04T00:00:00.000Z', + key: 1659571200000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-08-05T00:00:00.000Z', + key: 1659657600000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-08-06T00:00:00.000Z', + key: 1659744000000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-08-07T00:00:00.000Z', + key: 1659830400000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-08-08T00:00:00.000Z', + key: 1659916800000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-08-09T00:00:00.000Z', + key: 1660003200000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-08-10T00:00:00.000Z', + key: 1660089600000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-08-11T00:00:00.000Z', + key: 1660176000000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-08-12T00:00:00.000Z', + key: 1660262400000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-08-13T00:00:00.000Z', + key: 1660348800000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-08-14T00:00:00.000Z', + key: 1660435200000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-08-15T00:00:00.000Z', + key: 1660521600000, + doc_count: 0, + alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2022-08-16T00:00:00.000Z', + key: 1660608000000, + doc_count: 6, + alertStatus: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'recovered', doc_count: 6 }], + }, + }, + { + key_as_string: '2022-08-17T00:00:00.000Z', + key: 1660694400000, + doc_count: 2, + alertStatus: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'active', doc_count: 1 }, + { key: 'recovered', doc_count: 1 }, + ], + }, + }, + ], + }, + }, + }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx index f8092475f1ba0b..302392674ccddf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx @@ -15,6 +15,7 @@ import { EuiToolTip, EuiButtonIcon, EuiDataGridStyle, + EuiLoadingContent, } from '@elastic/eui'; import { useSorting, usePagination, useBulkActions } from './hooks'; import { AlertsTableProps } from '../../../types'; @@ -219,16 +220,21 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab (_props: EuiDataGridCellValueElementProps) => { // https://github.com/elastic/eui/issues/5811 const alert = alerts[_props.rowIndex - pagination.pageSize * pagination.pageIndex]; - const data: Array<{ field: string; value: string[] }> = []; - Object.entries(alert ?? {}).forEach(([key, value]) => { - data.push({ field: key, value: value as string[] }); - }); - return renderCellValue({ - ..._props, - data, - }); + if (alert) { + const data: Array<{ field: string; value: string[] }> = []; + Object.entries(alert ?? {}).forEach(([key, value]) => { + data.push({ field: key, value: value as string[] }); + }); + return renderCellValue({ + ..._props, + data, + }); + } else if (isLoading) { + return <EuiLoadingContent lines={1} />; + } + return null; }, - [alerts, pagination.pageIndex, pagination.pageSize, renderCellValue] + [alerts, isLoading, pagination.pageIndex, pagination.pageSize, renderCellValue] ); return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.test.tsx new file mode 100644 index 00000000000000..39264020f30a38 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.test.tsx @@ -0,0 +1,253 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { nextTick } from '@kbn/test-jest-helpers'; +import { RuleAlertsSummary } from './rule_alerts_summary'; +import { mount, ReactWrapper } from 'enzyme'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import { ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common'; +import { mockRule } from '../../../../mock/rule_details/alert_summary'; +import { AlertChartData } from './types'; + +jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({ + useUiSetting: jest.fn().mockImplementation(() => true), +})); + +jest.mock('../../../../hooks/use_load_rule_types', () => ({ + useLoadRuleTypes: jest.fn(), +})); + +jest.mock('../../../../hooks/use_load_rule_alerts_aggregations', () => ({ + useLoadRuleAlertsAggs: jest.fn().mockReturnValue({ + ruleAlertsAggs: { active: 1, recovered: 7 }, + alertsChartData: mockChartData(), + }), +})); + +const { useLoadRuleTypes } = jest.requireMock('../../../../hooks/use_load_rule_types'); +const ruleTypes = [ + { + id: 'test_rule_type', + name: 'some rule type', + actionGroups: [{ id: 'default', name: 'Default' }], + recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, + actionVariables: { context: [], state: [] }, + defaultActionGroupId: 'default', + producer: ALERTS_FEATURE_ID, + minimumLicenseRequired: 'basic', + enabledInLicense: true, + authorizedConsumers: { + [ALERTS_FEATURE_ID]: { read: true, all: false }, + }, + ruleTaskTimeout: '1m', + }, +]; + +describe('Rule Alert Summary', () => { + let wrapper: ReactWrapper; + + async function setup() { + const mockedRule = mockRule(); + + useLoadRuleTypes.mockReturnValue({ ruleTypes }); + + wrapper = mount( + <IntlProvider locale="en"> + <RuleAlertsSummary + rule={mockedRule} + filteredRuleTypes={['apm', 'uptime', 'metric', 'logs']} + /> + </IntlProvider> + ); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + } + beforeAll(async () => setup()); + it('should render the Rule Alerts Summary component', async () => { + expect(wrapper.find('[data-test-subj="ruleAlertsSummary"]')).toBeTruthy(); + }); + + it('should show zeros for all alerts counters', async () => { + expect(wrapper.find('[data-test-subj="activeAlertsCount"]').text()).toEqual('1'); + expect(wrapper.find('[data-test-subj="recoveredAlertsCount"]').text()).toBe('7'); + expect(wrapper.find('[data-test-subj="totalAlertsCount"]').text()).toBe('8'); + }); +}); + +// This function should stay in the same file as the test otherwise the test will fail. +function mockChartData(): AlertChartData[] { + return [ + { + date: 1660608000000, + count: 6, + status: 'recovered', + }, + { + date: 1660694400000, + count: 1, + status: 'recovered', + }, + { + date: 1660694400000, + count: 1, + status: 'active', + }, + { + date: 1658102400000, + count: 6, + status: 'total', + }, + { + date: 1658188800000, + count: 6, + status: 'total', + }, + { + date: 1658275200000, + count: 6, + status: 'total', + }, + { + date: 1658361600000, + count: 6, + status: 'total', + }, + { + date: 1658448000000, + count: 6, + status: 'total', + }, + { + date: 1658534400000, + count: 6, + status: 'total', + }, + { + date: 1658620800000, + count: 6, + status: 'total', + }, + { + date: 1658707200000, + count: 6, + status: 'total', + }, + { + date: 1658793600000, + count: 6, + status: 'total', + }, + { + date: 1658880000000, + count: 6, + status: 'total', + }, + { + date: 1658966400000, + count: 6, + status: 'total', + }, + { + date: 1659052800000, + count: 6, + status: 'total', + }, + { + date: 1659139200000, + count: 6, + status: 'total', + }, + { + date: 1659225600000, + count: 6, + status: 'total', + }, + { + date: 1659312000000, + count: 6, + status: 'total', + }, + { + date: 1659398400000, + count: 6, + status: 'total', + }, + { + date: 1659484800000, + count: 6, + status: 'total', + }, + { + date: 1659571200000, + count: 6, + status: 'total', + }, + { + date: 1659657600000, + count: 6, + status: 'total', + }, + { + date: 1659744000000, + count: 6, + status: 'total', + }, + { + date: 1659830400000, + count: 6, + status: 'total', + }, + { + date: 1659916800000, + count: 6, + status: 'total', + }, + { + date: 1660003200000, + count: 6, + status: 'total', + }, + { + date: 1660089600000, + count: 6, + status: 'total', + }, + { + date: 1660176000000, + count: 6, + status: 'total', + }, + { + date: 1660262400000, + count: 6, + status: 'total', + }, + { + date: 1660348800000, + count: 6, + status: 'total', + }, + { + date: 1660435200000, + count: 6, + status: 'total', + }, + { + date: 1660521600000, + count: 6, + status: 'total', + }, + { + date: 1660694400000, + count: 4, + status: 'total', + }, + ]; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.tsx index 6fc657d051594f..708b12ceb7b4ae 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.tsx @@ -98,6 +98,7 @@ export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummary if (errorRuleAlertsAggs) return ( <EuiEmptyPrompt + data-test-subj="alertsRuleSummaryErrorPrompt" iconType="alert" color="danger" title={ @@ -123,7 +124,7 @@ export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummary ); const isVisibleFunction: FilterPredicate = (series) => series.splitAccessors.get('g') !== 'total'; return ( - <EuiPanel hasShadow={false} hasBorder> + <EuiPanel data-test-subj="ruleAlertsSummary" hasShadow={false} hasBorder> <EuiFlexGroup direction="column"> <EuiFlexItem grow={false}> <EuiFlexGroup direction="column"> @@ -156,7 +157,7 @@ export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummary /> </EuiText> <EuiText> - <h4>{active + recovered}</h4> + <h4 data-test-subj="totalAlertsCount">{active + recovered}</h4> </EuiText> </EuiFlexItem> </EuiFlexGroup> @@ -169,7 +170,7 @@ export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummary /> </EuiText> <EuiText color={LIGHT_THEME.colors.vizColors[2]}> - <h4>{active}</h4> + <h4 data-test-subj="activeAlertsCount">{active}</h4> </EuiText> </EuiFlexItem> </EuiFlexGroup> @@ -183,7 +184,7 @@ export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummary </EuiText> <EuiFlexItem> <EuiText color={LIGHT_THEME.colors.vizColors[1]}> - <h4>{recovered}</h4> + <h4 data-test-subj="recoveredAlertsCount">{recovered}</h4> </EuiText> </EuiFlexItem> </EuiFlexItem> diff --git a/x-pack/plugins/upgrade_assistant/common/types.ts b/x-pack/plugins/upgrade_assistant/common/types.ts index 768ef135fe5551..74cc38e4f4df0f 100644 --- a/x-pack/plugins/upgrade_assistant/common/types.ts +++ b/x-pack/plugins/upgrade_assistant/common/types.ts @@ -6,7 +6,7 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { SavedObject, SavedObjectAttributes } from '@kbn/core/types'; +import { SavedObject } from '@kbn/core/types'; export type DeprecationSource = 'Kibana' | 'Elasticsearch'; @@ -57,7 +57,7 @@ export interface ReindexStatusResponse { export const REINDEX_OP_TYPE = 'upgrade-assistant-reindex-operation'; -export interface QueueSettings extends SavedObjectAttributes { +export interface QueueSettings { /** * A Unix timestamp of when the reindex operation was enqueued. * @@ -81,7 +81,7 @@ export interface QueueSettings extends SavedObjectAttributes { startedAt?: number; } -export interface ReindexOptions extends SavedObjectAttributes { +export interface ReindexOptions { /** * Whether to treat the index as if it were closed. This instructs the * reindex strategy to first open the index, perform reindexing and @@ -96,7 +96,7 @@ export interface ReindexOptions extends SavedObjectAttributes { queueSettings?: QueueSettings; } -export interface ReindexOperation extends SavedObjectAttributes { +export interface ReindexOperation { indexName: string; newIndexName: string; status: ReindexStatus; @@ -241,7 +241,7 @@ export interface ResolveIndexResponseFromES { export const ML_UPGRADE_OP_TYPE = 'upgrade-assistant-ml-upgrade-operation'; -export interface MlOperation extends SavedObjectAttributes { +export interface MlOperation { nodeId: string; snapshotId: string; jobId: string; diff --git a/x-pack/test/alerting_api_integration/common/lib/task_manager_utils.ts b/x-pack/test/alerting_api_integration/common/lib/task_manager_utils.ts index d8b1397edbd75e..19d62bdd0682a1 100644 --- a/x-pack/test/alerting_api_integration/common/lib/task_manager_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/task_manager_utils.ts @@ -20,6 +20,48 @@ export class TaskManagerUtils { this.retry = retry; } + async waitForDisabled(id: string, taskRunAtFilter: Date) { + return await this.retry.try(async () => { + const searchResult = await this.es.search({ + index: '.kibana_task_manager', + body: { + query: { + bool: { + must: [ + { + term: { + 'task.id': `task:${id}`, + }, + }, + { + terms: { + 'task.scope': ['actions', 'alerting'], + }, + }, + { + range: { + 'task.scheduledAt': { + gte: taskRunAtFilter.getTime().toString(), + }, + }, + }, + { + term: { + 'task.enabled': true, + }, + }, + ], + }, + }, + }, + }); + // @ts-expect-error + if (searchResult.hits.total.value) { + // @ts-expect-error + throw new Error(`Expected 0 tasks but received ${searchResult.hits.total.value}`); + } + }); + } async waitForEmpty(taskRunAtFilter: Date) { return await this.retry.try(async () => { const searchResult = await this.es.search({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts index e601c6ee15ec73..f775b3607fadee 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts @@ -137,6 +137,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { spaceId: space.id, consumer: 'alertsFixture', }); + expect(taskRecord.task.enabled).to.eql(true); // Ensure AAD isn't broken await checkAAD({ supertest, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/disable.ts index 842a00366945a0..2f452a54927b99 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/disable.ts @@ -16,6 +16,7 @@ import { ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, + TaskManagerDoc, } from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -30,11 +31,12 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte after(() => objectRemover.removeAll()); - async function getScheduledTask(id: string) { - return await es.get({ + async function getScheduledTask(id: string): Promise<TaskManagerDoc> { + const scheduledTask = await es.get<TaskManagerDoc>({ id: `task:${id}`, index: '.kibana_task_manager', }); + return scheduledTask._source!; } for (const scenario of UserAtSpaceScenarios) { @@ -88,8 +90,16 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte ), statusCode: 403, }); - // Ensure task still exists - await getScheduledTask(createdAlert.scheduled_task_id); + // Ensure task still exists and is still enabled + const taskRecord1 = await getScheduledTask(createdAlert.scheduled_task_id); + expect(taskRecord1.type).to.eql('task'); + expect(taskRecord1.task.taskType).to.eql('alerting:test.noop'); + expect(JSON.parse(taskRecord1.task.params)).to.eql({ + alertId: createdAlert.id, + spaceId: space.id, + consumer: 'alertsFixture', + }); + expect(taskRecord1.task.enabled).to.eql(true); break; case 'space_1_all_alerts_none_actions at space1': case 'superuser at space1': @@ -97,12 +107,17 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); - try { - await getScheduledTask(createdAlert.scheduled_task_id); - throw new Error('Should have removed scheduled task'); - } catch (e) { - expect(e.meta.statusCode).to.eql(404); - } + + // task should still exist but be disabled + const taskRecord2 = await getScheduledTask(createdAlert.scheduled_task_id); + expect(taskRecord2.type).to.eql('task'); + expect(taskRecord2.task.taskType).to.eql('alerting:test.noop'); + expect(JSON.parse(taskRecord2.task.params)).to.eql({ + alertId: createdAlert.id, + spaceId: space.id, + consumer: 'alertsFixture', + }); + expect(taskRecord2.task.enabled).to.eql(false); // Ensure AAD isn't broken await checkAAD({ supertest, @@ -153,12 +168,17 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); - try { - await getScheduledTask(createdAlert.scheduled_task_id); - throw new Error('Should have removed scheduled task'); - } catch (e) { - expect(e.meta.statusCode).to.eql(404); - } + + // task should still exist but be disabled + const taskRecord = await getScheduledTask(createdAlert.scheduled_task_id); + expect(taskRecord.type).to.eql('task'); + expect(taskRecord.task.taskType).to.eql('alerting:test.restricted-noop'); + expect(JSON.parse(taskRecord.task.params)).to.eql({ + alertId: createdAlert.id, + spaceId: space.id, + consumer: 'alertsRestrictedFixture', + }); + expect(taskRecord.task.enabled).to.eql(false); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); @@ -213,12 +233,16 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); - try { - await getScheduledTask(createdAlert.scheduled_task_id); - throw new Error('Should have removed scheduled task'); - } catch (e) { - expect(e.meta.statusCode).to.eql(404); - } + // task should still exist but be disabled + const taskRecord = await getScheduledTask(createdAlert.scheduled_task_id); + expect(taskRecord.type).to.eql('task'); + expect(taskRecord.task.taskType).to.eql('alerting:test.unrestricted-noop'); + expect(JSON.parse(taskRecord.task.params)).to.eql({ + alertId: createdAlert.id, + spaceId: space.id, + consumer: 'alertsFixture', + }); + expect(taskRecord.task.enabled).to.eql(false); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); @@ -269,12 +293,16 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); - try { - await getScheduledTask(createdAlert.scheduled_task_id); - throw new Error('Should have removed scheduled task'); - } catch (e) { - expect(e.meta.statusCode).to.eql(404); - } + // task should still exist but be disabled + const taskRecord = await getScheduledTask(createdAlert.scheduled_task_id); + expect(taskRecord.type).to.eql('task'); + expect(taskRecord.task.taskType).to.eql('alerting:test.noop'); + expect(JSON.parse(taskRecord.task.params)).to.eql({ + alertId: createdAlert.id, + spaceId: space.id, + consumer: 'alerts', + }); + expect(taskRecord.task.enabled).to.eql(false); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); @@ -319,8 +347,16 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte ), statusCode: 403, }); - // Ensure task still exists - await getScheduledTask(createdAlert.scheduled_task_id); + // Ensure task still exists and is still enabled + const taskRecord1 = await getScheduledTask(createdAlert.scheduled_task_id); + expect(taskRecord1.type).to.eql('task'); + expect(taskRecord1.task.taskType).to.eql('alerting:test.noop'); + expect(JSON.parse(taskRecord1.task.params)).to.eql({ + alertId: createdAlert.id, + spaceId: space.id, + consumer: 'alertsFixture', + }); + expect(taskRecord1.task.enabled).to.eql(true); break; case 'superuser at space1': case 'space_1_all at space1': @@ -328,12 +364,16 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); - try { - await getScheduledTask(createdAlert.scheduled_task_id); - throw new Error('Should have removed scheduled task'); - } catch (e) { - expect(e.meta.statusCode).to.eql(404); - } + // task should still exist but be disabled + const taskRecord2 = await getScheduledTask(createdAlert.scheduled_task_id); + expect(taskRecord2.type).to.eql('task'); + expect(taskRecord2.task.taskType).to.eql('alerting:test.noop'); + expect(JSON.parse(taskRecord2.task.params)).to.eql({ + alertId: createdAlert.id, + spaceId: space.id, + consumer: 'alertsFixture', + }); + expect(taskRecord2.task.enabled).to.eql(false); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/enable.ts index 0aba468174cffd..73842073a542b5 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/enable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/enable.ts @@ -129,6 +129,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex spaceId: space.id, consumer: 'alertsFixture', }); + expect(taskRecord.task.enabled).to.eql(true); // Ensure AAD isn't broken await checkAAD({ supertest, @@ -360,6 +361,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex spaceId: space.id, consumer: 'alertsFixture', }); + expect(taskRecord.task.enabled).to.eql(true); // Ensure AAD isn't broken await checkAAD({ supertest, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts index 19c10bddbd7b1c..c20e41067b0101 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts @@ -34,7 +34,7 @@ export default function alertTests({ getService }: FtrProviderContext) { const esTestIndexTool = new ESTestIndexTool(es, retry); const taskManagerUtils = new TaskManagerUtils(es, retry); - describe('alerts', () => { + describe('alerts test me', () => { const authorizationIndex = '.kibana-test-authorization'; const objectRemover = new ObjectRemover(supertest); @@ -122,7 +122,7 @@ export default function alertTests({ getService }: FtrProviderContext) { const alertId = response.body.id; await alertUtils.disable(alertId); - await taskManagerUtils.waitForEmpty(testStart); + await taskManagerUtils.waitForDisabled(alertId, testStart); // Ensure only 1 alert executed with proper params const alertSearchResult = await esTestIndexTool.search( @@ -274,7 +274,7 @@ instanceStateValue: true const alertId = response.body.id; await alertUtils.disable(alertId); - await taskManagerUtils.waitForEmpty(testStart); + await taskManagerUtils.waitForDisabled(alertId, testStart); // Ensure only 1 alert executed with proper params const alertSearchResult = await esTestIndexTool.search( @@ -634,7 +634,7 @@ instanceStateValue: true // Wait for test.authorization to index a document before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('alert:test.authorization', reference); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForEmpty(testStart); + await taskManagerUtils.waitForDisabled(response.body.id, testStart); // Ensure only 1 document exists with proper params searchResult = await esTestIndexTool.search('alert:test.authorization', reference); @@ -665,7 +665,7 @@ instanceStateValue: true // Wait for test.authorization to index a document before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('alert:test.authorization', reference); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForEmpty(testStart); + await taskManagerUtils.waitForDisabled(response.body.id, testStart); // Ensure only 1 document exists with proper params searchResult = await esTestIndexTool.search('alert:test.authorization', reference); @@ -751,7 +751,7 @@ instanceStateValue: true // Ensure test.authorization indexed 1 document before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('action:test.authorization', reference); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForEmpty(testStart); + await taskManagerUtils.waitForDisabled(response.body.id, testStart); // Ensure only 1 document with proper params exists searchResult = await esTestIndexTool.search('action:test.authorization', reference); @@ -790,7 +790,7 @@ instanceStateValue: true // Ensure test.authorization indexed 1 document before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('action:test.authorization', reference); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForEmpty(testStart); + await taskManagerUtils.waitForDisabled(response.body.id, testStart); // Ensure only 1 document with proper params exists searchResult = await esTestIndexTool.search('action:test.authorization', reference); @@ -853,7 +853,7 @@ instanceStateValue: true // Wait until alerts scheduled actions 3 times before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('alert:test.always-firing', reference, 3); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForEmpty(testStart); + await taskManagerUtils.waitForDisabled(response.body.id, testStart); // Ensure actions only executed once const searchResult = await esTestIndexTool.search( @@ -933,7 +933,7 @@ instanceStateValue: true // Wait for actions to execute twice before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('action:test.index-record', reference, 2); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForEmpty(testStart); + await taskManagerUtils.waitForDisabled(response.body.id, testStart); // Ensure only 2 actions with proper params exists const searchResult = await esTestIndexTool.search( @@ -1009,7 +1009,7 @@ instanceStateValue: true // Wait for actions to execute twice before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('action:test.index-record', reference, 2); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForEmpty(testStart); + await taskManagerUtils.waitForDisabled(response.body.id, testStart); // Ensure only 2 actions with proper params exists const searchResult = await esTestIndexTool.search( @@ -1074,7 +1074,7 @@ instanceStateValue: true // Actions should execute twice before widning things down await esTestIndexTool.waitForDocs('action:test.index-record', reference, 2); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForEmpty(testStart); + await taskManagerUtils.waitForDisabled(response.body.id, testStart); // Ensure only 2 actions are executed const searchResult = await esTestIndexTool.search( @@ -1133,7 +1133,7 @@ instanceStateValue: true // execution once before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('alert:test.always-firing', reference, 2); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForEmpty(testStart); + await taskManagerUtils.waitForDisabled(response.body.id, testStart); // Should not have executed any action const executedActionsResult = await esTestIndexTool.search( @@ -1192,7 +1192,7 @@ instanceStateValue: true // once before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('alert:test.always-firing', reference, 2); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForEmpty(testStart); + await taskManagerUtils.waitForDisabled(response.body.id, testStart); // Should not have executed any action const executedActionsResult = await esTestIndexTool.search( @@ -1252,7 +1252,7 @@ instanceStateValue: true // Ensure actions are executed once before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('action:test.index-record', reference, 1); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForEmpty(testStart); + await taskManagerUtils.waitForDisabled(response.body.id, testStart); // Should have one document indexed by the action const searchResult = await esTestIndexTool.search( diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts index 143d845d074c4f..7860bf15dc8e53 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts @@ -108,6 +108,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { spaceId: Spaces.space1.id, consumer: 'alertsFixture', }); + expect(taskRecord.task.enabled).to.eql(true); // Ensure AAD isn't broken await checkAAD({ supertest, @@ -498,6 +499,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { spaceId: Spaces.space1.id, consumer: 'alertsFixture', }); + expect(taskRecord.task.enabled).to.eql(true); // Ensure AAD isn't broken await checkAAD({ supertest, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts index 6df7f4b3f6de8e..feec6431ee3cf9 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts @@ -15,6 +15,7 @@ import { getTestRuleData, ObjectRemover, getEventLog, + TaskManagerDoc, } from '../../../common/lib'; import { validateEvent } from './event_log'; @@ -31,11 +32,12 @@ export default function createDisableRuleTests({ getService }: FtrProviderContex after(() => objectRemover.removeAll()); - async function getScheduledTask(id: string) { - return await es.get({ + async function getScheduledTask(id: string): Promise<TaskManagerDoc> { + const scheduledTask = await es.get<TaskManagerDoc>({ id: `task:${id}`, index: '.kibana_task_manager', }); + return scheduledTask._source!; } it('should handle disable rule request appropriately', async () => { @@ -48,12 +50,16 @@ export default function createDisableRuleTests({ getService }: FtrProviderContex await ruleUtils.disable(createdRule.id); - try { - await getScheduledTask(createdRule.scheduled_task_id); - throw new Error('Should have removed scheduled task'); - } catch (e) { - expect(e.meta.statusCode).to.eql(404); - } + // task doc should still exist but be disabled + const taskRecord = await getScheduledTask(createdRule.scheduled_task_id); + expect(taskRecord.type).to.eql('task'); + expect(taskRecord.task.taskType).to.eql('alerting:test.noop'); + expect(JSON.parse(taskRecord.task.params)).to.eql({ + alertId: createdRule.id, + spaceId: Spaces.space1.id, + consumer: 'alertsFixture', + }); + expect(taskRecord.task.enabled).to.eql(false); // Ensure AAD isn't broken await checkAAD({ @@ -188,12 +194,16 @@ export default function createDisableRuleTests({ getService }: FtrProviderContex .set('kbn-xsrf', 'foo') .expect(204); - try { - await getScheduledTask(createdRule.scheduled_task_id); - throw new Error('Should have removed scheduled task'); - } catch (e) { - expect(e.meta.statusCode).to.eql(404); - } + // task doc should still exist but be disabled + const taskRecord = await getScheduledTask(createdRule.scheduled_task_id); + expect(taskRecord.type).to.eql('task'); + expect(taskRecord.task.taskType).to.eql('alerting:test.noop'); + expect(JSON.parse(taskRecord.task.params)).to.eql({ + alertId: createdRule.id, + spaceId: Spaces.space1.id, + consumer: 'alertsFixture', + }); + expect(taskRecord.task.enabled).to.eql(false); // Ensure AAD isn't broken await checkAAD({ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts index 59ae5efcba1919..d8dec2a486298b 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts @@ -59,6 +59,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex spaceId: Spaces.space1.id, consumer: 'alertsFixture', }); + expect(taskRecord.task.enabled).to.eql(true); // Ensure AAD isn't broken await checkAAD({ @@ -111,6 +112,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex spaceId: Spaces.space1.id, consumer: 'alertsFixture', }); + expect(taskRecord.task.enabled).to.eql(true); // Ensure AAD isn't broken await checkAAD({ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_execution_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_execution_log.ts index 1f33f44a20d6a0..984b6bbb832f91 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_execution_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_execution_log.ts @@ -74,6 +74,7 @@ export default function createGetExecutionLogTests({ getService }: FtrProviderCo let previousTimestamp: string | null = null; for (const log of execLogs) { + expect(log.rule_name).to.equal('abc'); if (previousTimestamp) { // default sort is `desc` by timestamp expect(Date.parse(log.timestamp)).to.be.lessThan(Date.parse(previousTimestamp)); @@ -177,6 +178,7 @@ export default function createGetExecutionLogTests({ getService }: FtrProviderCo expect(execLogs.length).to.eql(1); for (const log of execLogs) { + expect(log.rule_name).to.equal('abc'); expect(log.duration_ms).to.be.greaterThan(0); expect(log.schedule_delay_ms).to.be.greaterThan(0); expect(log.status).to.equal('success'); @@ -314,6 +316,7 @@ export default function createGetExecutionLogTests({ getService }: FtrProviderCo expect(execLogs.length).to.eql(1); for (const log of execLogs) { + expect(log.rule_name).to.equal('abc'); expect(log.status).to.equal('success'); expect(log.num_active_alerts).to.equal(1); @@ -372,6 +375,7 @@ export default function createGetExecutionLogTests({ getService }: FtrProviderCo expect(execLogs.length).to.eql(1); for (const log of execLogs) { + expect(log.rule_name).to.equal('abc'); expect(log.status).to.equal('success'); expect(log.num_active_alerts).to.equal(1); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/scheduled_task_id.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/scheduled_task_id.ts index 607166203e35ff..d008421381b14e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/scheduled_task_id.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/scheduled_task_id.ts @@ -109,6 +109,7 @@ export default function createScheduledTaskIdTests({ getService }: FtrProviderCo spaceId: 'default', consumer: 'alertsFixture', }); + expect(taskRecord.task.enabled).to.eql(true); }); }); } diff --git a/x-pack/test/api_integration/apis/cases/bulk_get_user_profiles.ts b/x-pack/test/api_integration/apis/cases/bulk_get_user_profiles.ts index a8aad06c999d0c..8de4b3dc32a0c6 100644 --- a/x-pack/test/api_integration/apis/cases/bulk_get_user_profiles.ts +++ b/x-pack/test/api_integration/apis/cases/bulk_get_user_profiles.ts @@ -11,11 +11,11 @@ import { APP_ID as SECURITY_SOLUTION_APP_ID } from '@kbn/security-solution-plugi import { observabilityFeatureId as OBSERVABILITY_APP_ID } from '@kbn/observability-plugin/common'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { deleteAllCaseItems } from '../../../cases_api_integration/common/lib/utils'; import { - deleteAllCaseItems, + bulkGetUserProfiles, suggestUserProfiles, -} from '../../../cases_api_integration/common/lib/utils'; -import { bulkGetUserProfiles } from '../../../cases_api_integration/common/lib/user_profiles'; +} from '../../../cases_api_integration/common/lib/user_profiles'; import { casesAllUser, casesReadUser, diff --git a/x-pack/test/api_integration/apis/cases/index.ts b/x-pack/test/api_integration/apis/cases/index.ts index 3bb170937bafcb..5b9d9d1bfe918e 100644 --- a/x-pack/test/api_integration/apis/cases/index.ts +++ b/x-pack/test/api_integration/apis/cases/index.ts @@ -10,7 +10,7 @@ import { deleteUsersAndRoles, } from '../../../cases_api_integration/common/lib/authentication'; -import { loginUsers } from '../../../cases_api_integration/common/lib/utils'; +import { loginUsers } from '../../../cases_api_integration/common/lib/user_profiles'; import { casesAllUser, obsCasesAllUser, secAllUser, users } from './common/users'; import { roles } from './common/roles'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/apis/cases/suggest_user_profiles.ts b/x-pack/test/api_integration/apis/cases/suggest_user_profiles.ts index 72680ef786e9ed..dca999ab689027 100644 --- a/x-pack/test/api_integration/apis/cases/suggest_user_profiles.ts +++ b/x-pack/test/api_integration/apis/cases/suggest_user_profiles.ts @@ -11,15 +11,16 @@ import { APP_ID as SECURITY_SOLUTION_APP_ID } from '@kbn/security-solution-plugi import { observabilityFeatureId as OBSERVABILITY_APP_ID } from '@kbn/observability-plugin/common'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { - deleteAllCaseItems, - suggestUserProfiles, -} from '../../../cases_api_integration/common/lib/utils'; +import { deleteAllCaseItems } from '../../../cases_api_integration/common/lib/utils'; +import { suggestUserProfiles } from '../../../cases_api_integration/common/lib/user_profiles'; import { casesAllUser, casesOnlyDeleteUser, + casesReadUser, obsCasesAllUser, obsCasesOnlyDeleteUser, + obsCasesReadUser, + secAllCasesNoneUser, secAllCasesReadUser, secAllUser, } from './common/users'; @@ -33,27 +34,34 @@ export default ({ getService }: FtrProviderContext): void => { await deleteAllCaseItems(es); }); - for (const { user, owner } of [ - { user: secAllUser, owner: SECURITY_SOLUTION_APP_ID }, - { user: casesAllUser, owner: CASES_APP_ID }, - { user: obsCasesAllUser, owner: OBSERVABILITY_APP_ID }, + for (const { user, searchTerm, owner } of [ + { user: secAllUser, searchTerm: secAllUser.username, owner: SECURITY_SOLUTION_APP_ID }, + { + user: secAllCasesReadUser, + searchTerm: secAllUser.username, + owner: SECURITY_SOLUTION_APP_ID, + }, + { user: casesAllUser, searchTerm: casesAllUser.username, owner: CASES_APP_ID }, + { user: casesReadUser, searchTerm: casesAllUser.username, owner: CASES_APP_ID }, + { user: obsCasesAllUser, searchTerm: obsCasesAllUser.username, owner: OBSERVABILITY_APP_ID }, + { user: obsCasesReadUser, searchTerm: obsCasesAllUser.username, owner: OBSERVABILITY_APP_ID }, ]) { it(`User ${ user.username } with roles(s) ${user.roles.join()} can retrieve user profile suggestions`, async () => { const profiles = await suggestUserProfiles({ supertest: supertestWithoutAuth, - req: { name: user.username, owners: [owner], size: 1 }, + req: { name: searchTerm, owners: [owner], size: 1 }, auth: { user, space: null }, }); expect(profiles.length).to.be(1); - expect(profiles[0].user.username).to.eql(user.username); + expect(profiles[0].user.username).to.eql(searchTerm); }); } for (const { user, owner } of [ - { user: secAllCasesReadUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: secAllCasesNoneUser, owner: SECURITY_SOLUTION_APP_ID }, { user: casesOnlyDeleteUser, owner: CASES_APP_ID }, { user: obsCasesOnlyDeleteUser, owner: OBSERVABILITY_APP_ID }, ]) { diff --git a/x-pack/test/api_integration/apis/uptime/rest/edit_monitor.ts b/x-pack/test/api_integration/apis/uptime/rest/edit_monitor.ts index 480d07e7144f3c..7bcda35a892f95 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/edit_monitor.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/edit_monitor.ts @@ -16,7 +16,8 @@ import { getFixtureJson } from './helper/get_fixture_json'; import { PrivateLocationTestService } from './services/private_location_test_service'; export default function ({ getService }: FtrProviderContext) { - describe('[PUT] /internal/uptime/service/monitors', function () { + // Failing: See https://github.com/elastic/kibana/issues/140520 + describe.skip('[PUT] /internal/uptime/service/monitors', function () { this.tags('skipCloud'); const supertest = getService('supertest'); diff --git a/x-pack/test/api_integration/apis/uptime/rest/sample_data/test_policy.ts b/x-pack/test/api_integration/apis/uptime/rest/sample_data/test_policy.ts index 5e6795ba86ef06..f24c839ddb296a 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/sample_data/test_policy.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/sample_data/test_policy.ts @@ -30,6 +30,7 @@ export const getTestSyntheticsPolicy = ( { enabled: true, data_stream: { type: 'synthetics', dataset: 'http' }, + release: 'experimental', vars: { __ui: { value: @@ -129,6 +130,7 @@ export const getTestSyntheticsPolicy = ( streams: [ { enabled: false, + release: 'experimental', data_stream: { type: 'synthetics', dataset: 'tcp' }, vars: { __ui: { type: 'yaml' }, @@ -167,6 +169,7 @@ export const getTestSyntheticsPolicy = ( streams: [ { enabled: false, + release: 'experimental', data_stream: { type: 'synthetics', dataset: 'icmp' }, vars: { __ui: { type: 'yaml' }, @@ -196,6 +199,7 @@ export const getTestSyntheticsPolicy = ( streams: [ { enabled: true, + release: 'beta', data_stream: { type: 'synthetics', dataset: 'browser' }, vars: { __ui: { type: 'yaml' }, @@ -253,6 +257,7 @@ export const getTestSyntheticsPolicy = ( { enabled: true, data_stream: { type: 'synthetics', dataset: 'browser.network' }, + release: 'beta', id: 'synthetics/browser-browser.network-2bfd7da0-22ed-11ed-8c6b-09a2d21dfbc3-27337270-22ed-11ed-8c6b-09a2d21dfbc3-default', compiled_stream: { processors: [ @@ -264,6 +269,7 @@ export const getTestSyntheticsPolicy = ( { enabled: true, data_stream: { type: 'synthetics', dataset: 'browser.screenshot' }, + release: 'beta', id: 'synthetics/browser-browser.screenshot-2bfd7da0-22ed-11ed-8c6b-09a2d21dfbc3-27337270-22ed-11ed-8c6b-09a2d21dfbc3-default', compiled_stream: { processors: [ @@ -317,6 +323,7 @@ export const getTestProjectSyntheticsPolicy = ( { enabled: false, data_stream: { type: 'synthetics', dataset: 'http' }, + release: 'experimental', vars: { __ui: { type: 'yaml' }, enabled: { value: true, type: 'bool' }, @@ -364,6 +371,7 @@ export const getTestProjectSyntheticsPolicy = ( { enabled: false, data_stream: { type: 'synthetics', dataset: 'tcp' }, + release: 'experimental', vars: { __ui: { type: 'yaml' }, enabled: { value: true, type: 'bool' }, @@ -401,6 +409,7 @@ export const getTestProjectSyntheticsPolicy = ( streams: [ { enabled: false, + release: 'experimental', data_stream: { type: 'synthetics', dataset: 'icmp' }, vars: { __ui: { type: 'yaml' }, @@ -431,6 +440,7 @@ export const getTestProjectSyntheticsPolicy = ( { enabled: true, data_stream: { type: 'synthetics', dataset: 'browser' }, + release: 'beta', vars: { __ui: { value: @@ -522,6 +532,7 @@ export const getTestProjectSyntheticsPolicy = ( }, { enabled: true, + release: 'beta', data_stream: { type: 'synthetics', dataset: 'browser.network' }, id: 'synthetics/browser-browser.network-4b6abc6c-118b-4d93-a489-1135500d09f1-test-suite-default-d70a46e0-22ea-11ed-8c6b-09a2d21dfbc3', compiled_stream: { @@ -533,6 +544,7 @@ export const getTestProjectSyntheticsPolicy = ( }, { enabled: true, + release: 'beta', data_stream: { type: 'synthetics', dataset: 'browser.screenshot' }, id: 'synthetics/browser-browser.screenshot-4b6abc6c-118b-4d93-a489-1135500d09f1-test-suite-default-d70a46e0-22ea-11ed-8c6b-09a2d21dfbc3', compiled_stream: { diff --git a/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts index 43ba70cc100a15..53c5fec2bd5bfa 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts @@ -38,7 +38,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { let ruleId: string | undefined; before(async () => { - const serviceA = apm.service('a', 'production', 'java').instance('a'); + const serviceA = apm + .service({ name: 'a', environment: 'production', agentName: 'java' }) + .instance('a'); const events = timerange(new Date(start).getTime(), new Date(end).getTime()) .interval('1m') @@ -52,7 +54,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { return [ ...range(0, count).flatMap((_) => serviceA - .transaction('tx', 'request') + .transaction({ transactionName: 'tx' }) .timestamp(timestamp) .duration(duration) .outcome(outcome) diff --git a/x-pack/test/apm_api_integration/tests/anomalies/anomaly_charts.spec.ts b/x-pack/test/apm_api_integration/tests/anomalies/anomaly_charts.spec.ts index 0416a0124627af..80a5474ae41b24 100644 --- a/x-pack/test/apm_api_integration/tests/anomalies/anomaly_charts.spec.ts +++ b/x-pack/test/apm_api_integration/tests/anomalies/anomaly_charts.spec.ts @@ -101,9 +101,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { const NORMAL_RATE = 1; before(async () => { - const serviceA = apm.service('a', 'production', 'java').instance('a'); + const serviceA = apm + .service({ name: 'a', environment: 'production', agentName: 'java' }) + .instance('a'); - const serviceB = apm.service('b', 'development', 'go').instance('b'); + const serviceB = apm + .service({ name: 'b', environment: 'development', agentName: 'go' }) + .instance('b'); const events = timerange(new Date(start).getTime(), new Date(end).getTime()) .interval('1m') @@ -117,13 +121,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { return [ ...range(0, count).flatMap((_) => serviceA - .transaction('tx', 'request') + .transaction({ transactionName: 'tx', transactionType: 'request' }) .timestamp(timestamp) .duration(duration) .outcome(outcome) ), serviceB - .transaction('tx', 'Worker') + .transaction({ transactionName: 'tx', transactionType: 'Worker' }) .timestamp(timestamp) .duration(duration) .success(), diff --git a/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/generate_data.ts b/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/generate_data.ts index 4effd2ed23d7c3..51b3a198667b0b 100644 --- a/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/generate_data.ts @@ -27,14 +27,16 @@ export async function generateData({ warmStartRate: number; }) { const { transactionName, duration, serviceName } = dataConfig; - const instance = apm.service(serviceName, 'production', 'go').instance('instance-a'); + const instance = apm + .service({ name: serviceName, environment: 'production', agentName: 'go' }) + .instance('instance-a'); const traceEvents = timerange(start, end) .interval('1m') .rate(coldStartRate) .generator((timestamp) => instance - .transaction(transactionName) + .transaction({ transactionName }) .defaults({ 'faas.coldstart': true, }) @@ -48,7 +50,7 @@ export async function generateData({ .rate(warmStartRate) .generator((timestamp) => instance - .transaction(transactionName) + .transaction({ transactionName }) .defaults({ 'faas.coldstart': false, }) diff --git a/x-pack/test/apm_api_integration/tests/cold_start/generate_data.ts b/x-pack/test/apm_api_integration/tests/cold_start/generate_data.ts index 4baa9fd877f10f..b7169d3aedc54f 100644 --- a/x-pack/test/apm_api_integration/tests/cold_start/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/cold_start/generate_data.ts @@ -33,7 +33,9 @@ export async function generateData({ warmStartRate: number; }) { const { coldStartTransaction, warmStartTransaction, serviceName } = dataConfig; - const instance = apm.service(serviceName, 'production', 'go').instance('instance-a'); + const instance = apm + .service({ name: serviceName, environment: 'production', agentName: 'go' }) + .instance('instance-a'); const traceEvents = [ timerange(start, end) @@ -41,7 +43,7 @@ export async function generateData({ .rate(coldStartRate) .generator((timestamp) => instance - .transaction(coldStartTransaction.name) + .transaction({ transactionName: coldStartTransaction.name }) .defaults({ 'faas.coldstart': true, }) @@ -54,7 +56,7 @@ export async function generateData({ .rate(warmStartRate) .generator((timestamp) => instance - .transaction(warmStartTransaction.name) + .transaction({ transactionName: warmStartTransaction.name }) .defaults({ 'faas.coldstart': false, }) diff --git a/x-pack/test/apm_api_integration/tests/data_view/static.spec.ts b/x-pack/test/apm_api_integration/tests/data_view/static.spec.ts index a77a2e443b9d5e..11bb01d3ca7bb9 100644 --- a/x-pack/test/apm_api_integration/tests/data_view/static.spec.ts +++ b/x-pack/test/apm_api_integration/tests/data_view/static.spec.ts @@ -136,14 +136,20 @@ function generateApmData(synthtrace: ApmSynthtraceEsClient) { new Date('2021-10-01T00:01:00.000Z').getTime() ); - const instance = apm.service('multiple-env-service', 'production', 'go').instance('my-instance'); + const instance = apm + .service({ name: 'multiple-env-service', environment: 'production', agentName: 'go' }) + .instance('my-instance'); return synthtrace.index([ range .interval('1s') .rate(1) .generator((timestamp) => - instance.transaction('GET /api').timestamp(timestamp).duration(30).success() + instance + .transaction({ transactionName: 'GET /api' }) + .timestamp(timestamp) + .duration(30) + .success() ), ]); } diff --git a/x-pack/test/apm_api_integration/tests/dependencies/generate_data.ts b/x-pack/test/apm_api_integration/tests/dependencies/generate_data.ts index 1efab799963881..c7a5c43b05c05f 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/generate_data.ts @@ -30,7 +30,9 @@ export async function generateData({ start: number; end: number; }) { - const instance = apm.service('synth-go', 'production', 'go').instance('instance-a'); + const instance = apm + .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) + .instance('instance-a'); const { rate, transaction, span } = dataConfig; await synthtraceEsClient.index( @@ -39,13 +41,13 @@ export async function generateData({ .rate(rate) .generator((timestamp) => instance - .transaction(transaction.name) + .transaction({ transactionName: transaction.name }) .timestamp(timestamp) .duration(transaction.duration) .success() .children( instance - .span(span.name, span.type, span.subType) + .span({ spanName: span.name, spanType: span.type, spanSubtype: span.subType }) .duration(transaction.duration) .success() .destination(span.destination) diff --git a/x-pack/test/apm_api_integration/tests/dependencies/generate_operation_data.ts b/x-pack/test/apm_api_integration/tests/dependencies/generate_operation_data.ts index 7724b5fe334b66..7537e310bff22b 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/generate_operation_data.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/generate_operation_data.ts @@ -27,8 +27,12 @@ export async function generateOperationData({ end: number; synthtraceEsClient: ApmSynthtraceEsClient; }) { - const synthGoInstance = apm.service('synth-go', 'production', 'go').instance('instance-a'); - const synthJavaInstance = apm.service('synth-java', 'development', 'java').instance('instance-a'); + const synthGoInstance = apm + .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) + .instance('instance-a'); + const synthJavaInstance = apm + .service({ name: 'synth-java', environment: 'development', agentName: 'java' }) + .instance('instance-a'); const interval = timerange(start, end).interval('1m'); @@ -37,7 +41,7 @@ export async function generateOperationData({ .rate(generateOperationDataConfig.ES_SEARCH_UNKNOWN_RATE) .generator((timestamp) => synthGoInstance - .span('/_search', 'db', 'elasticsearch') + .span({ spanName: '/_search', spanType: 'db', spanSubtype: 'elasticsearch' }) .destination('elasticsearch') .timestamp(timestamp) .duration(generateOperationDataConfig.ES_SEARCH_DURATION) @@ -46,7 +50,7 @@ export async function generateOperationData({ .rate(generateOperationDataConfig.ES_SEARCH_SUCCESS_RATE) .generator((timestamp) => synthGoInstance - .span('/_search', 'db', 'elasticsearch') + .span({ spanName: '/_search', spanType: 'db', spanSubtype: 'elasticsearch' }) .destination('elasticsearch') .timestamp(timestamp) .success() @@ -56,7 +60,7 @@ export async function generateOperationData({ .rate(generateOperationDataConfig.ES_SEARCH_FAILURE_RATE) .generator((timestamp) => synthGoInstance - .span('/_search', 'db', 'elasticsearch') + .span({ spanName: '/_search', spanType: 'db', spanSubtype: 'elasticsearch' }) .destination('elasticsearch') .timestamp(timestamp) .failure() @@ -66,7 +70,7 @@ export async function generateOperationData({ .rate(generateOperationDataConfig.ES_BULK_RATE) .generator((timestamp) => synthJavaInstance - .span('/_bulk', 'db', 'elasticsearch') + .span({ spanName: '/_bulk', spanType: 'db', spanSubtype: 'elasticsearch' }) .destination('elasticsearch') .timestamp(timestamp) .duration(generateOperationDataConfig.ES_BULK_DURATION) @@ -75,7 +79,7 @@ export async function generateOperationData({ .rate(generateOperationDataConfig.REDIS_SET_RATE) .generator((timestamp) => synthJavaInstance - .span('SET', 'db', 'redis') + .span({ spanName: 'SET', spanType: 'db', spanSubtype: 'redis' }) .destination('redis') .timestamp(timestamp) .duration(generateOperationDataConfig.REDIS_SET_DURATION) diff --git a/x-pack/test/apm_api_integration/tests/dependencies/top_spans.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/top_spans.spec.ts index 06890c0b6fd592..e93af3051d4514 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/top_spans.spec.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/top_spans.spec.ts @@ -70,9 +70,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { 'Top dependency spans when data is loaded', { config: 'basic', archives: [] }, () => { - const javaInstance = apm.service('java', 'production', 'java').instance('instance-a'); + const javaInstance = apm + .service({ name: 'java', environment: 'production', agentName: 'java' }) + .instance('instance-a'); - const goInstance = apm.service('go', 'development', 'go').instance('instance-a'); + const goInstance = apm + .service({ name: 'go', environment: 'development', agentName: 'go' }) + .instance('instance-a'); before(async () => { await synthtraceEsClient.index([ @@ -81,40 +85,48 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(1) .generator((timestamp) => [ javaInstance - .span('without transaction', 'db', 'elasticsearch') + .span({ + spanName: 'without transaction', + spanType: 'db', + spanSubtype: 'elasticsearch', + }) .destination('elasticsearch') .duration(200) .timestamp(timestamp), javaInstance - .transaction('GET /api/my-endpoint') + .transaction({ transactionName: 'GET /api/my-endpoint' }) .duration(100) .timestamp(timestamp) .children( javaInstance - .span('/_search', 'db', 'elasticsearch') + .span({ spanName: '/_search', spanType: 'db', spanSubtype: 'elasticsearch' }) .destination('elasticsearch') .duration(100) .success() .timestamp(timestamp) ), goInstance - .transaction('GET /api/my-other-endpoint') + .transaction({ transactionName: 'GET /api/my-other-endpoint' }) .duration(100) .timestamp(timestamp) .children( goInstance - .span('/_search', 'db', 'elasticsearch') + .span({ spanName: '/_search', spanType: 'db', spanSubtype: 'elasticsearch' }) .destination('elasticsearch') .duration(50) .timestamp(timestamp) ), goInstance - .transaction('GET /api/my-other-endpoint') + .transaction({ transactionName: 'GET /api/my-other-endpoint' }) .duration(100) .timestamp(timestamp) .children( goInstance - .span('/_search', 'db', 'fake-elasticsearch') + .span({ + spanName: '/_search', + spanType: 'db', + spanSubtype: 'fake-elasticsearch', + }) .destination('fake-elasticsearch') .duration(50) .timestamp(timestamp) diff --git a/x-pack/test/apm_api_integration/tests/error_rate/service_apis.spec.ts b/x-pack/test/apm_api_integration/tests/error_rate/service_apis.spec.ts index fe2f9c8cd2cf80..8b735b51d1e68a 100644 --- a/x-pack/test/apm_api_integration/tests/error_rate/service_apis.spec.ts +++ b/x-pack/test/apm_api_integration/tests/error_rate/service_apis.spec.ts @@ -122,7 +122,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const GO_PROD_ID_ERROR_RATE = 50; before(async () => { const serviceGoProdInstance = apm - .service(serviceName, 'production', 'go') + .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance('instance-a'); const transactionNameProductList = 'GET /api/product/list'; @@ -134,7 +134,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_PROD_LIST_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction(transactionNameProductList) + .transaction({ transactionName: transactionNameProductList }) .timestamp(timestamp) .duration(1000) .success() @@ -144,7 +144,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_PROD_LIST_ERROR_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction(transactionNameProductList) + .transaction({ transactionName: transactionNameProductList }) .duration(1000) .timestamp(timestamp) .failure() @@ -154,7 +154,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_PROD_ID_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction(transactionNameProductId) + .transaction({ transactionName: transactionNameProductId }) .timestamp(timestamp) .duration(1000) .success() @@ -164,7 +164,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_PROD_ID_ERROR_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction(transactionNameProductId) + .transaction({ transactionName: transactionNameProductId }) .duration(1000) .timestamp(timestamp) .failure() diff --git a/x-pack/test/apm_api_integration/tests/error_rate/service_maps.spec.ts b/x-pack/test/apm_api_integration/tests/error_rate/service_maps.spec.ts index 8c14a203f78007..3610b240572aaf 100644 --- a/x-pack/test/apm_api_integration/tests/error_rate/service_maps.spec.ts +++ b/x-pack/test/apm_api_integration/tests/error_rate/service_maps.spec.ts @@ -68,7 +68,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const GO_PROD_ID_ERROR_RATE = 50; before(async () => { const serviceGoProdInstance = apm - .service(serviceName, 'production', 'go') + .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance('instance-a'); const transactionNameProductList = 'GET /api/product/list'; @@ -80,7 +80,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_PROD_LIST_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction(transactionNameProductList, 'Worker') + .transaction({ + transactionName: transactionNameProductList, + transactionType: 'Worker', + }) .timestamp(timestamp) .duration(1000) .success() @@ -90,7 +93,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_PROD_LIST_ERROR_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction(transactionNameProductList, 'Worker') + .transaction({ + transactionName: transactionNameProductList, + transactionType: 'Worker', + }) .duration(1000) .timestamp(timestamp) .failure() @@ -100,7 +106,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_PROD_ID_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction(transactionNameProductId) + .transaction({ transactionName: transactionNameProductId }) .timestamp(timestamp) .duration(1000) .success() @@ -110,7 +116,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_PROD_ID_ERROR_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction(transactionNameProductId) + .transaction({ transactionName: transactionNameProductId }) .duration(1000) .timestamp(timestamp) .failure() diff --git a/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts b/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts index 5156ed7f054786..534850d2cc927c 100644 --- a/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts +++ b/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts @@ -68,7 +68,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { }; before(async () => { - const serviceInstance = apm.service(serviceName, 'production', 'go').instance('instance-a'); + const serviceInstance = apm + .service({ name: serviceName, environment: 'production', agentName: 'go' }) + .instance('instance-a'); await synthtraceEsClient.index([ timerange(start, end) @@ -76,7 +78,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(appleTransaction.successRate) .generator((timestamp) => serviceInstance - .transaction(appleTransaction.name) + .transaction({ transactionName: appleTransaction.name }) .timestamp(timestamp) .duration(1000) .success() @@ -86,8 +88,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(appleTransaction.failureRate) .generator((timestamp) => serviceInstance - .transaction(appleTransaction.name) - .errors(serviceInstance.error('error 1', 'foo').timestamp(timestamp)) + .transaction({ transactionName: appleTransaction.name }) + .errors( + serviceInstance.error({ message: 'error 1', type: 'foo' }).timestamp(timestamp) + ) .duration(1000) .timestamp(timestamp) .failure() @@ -97,7 +101,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(bananaTransaction.successRate) .generator((timestamp) => serviceInstance - .transaction(bananaTransaction.name) + .transaction({ transactionName: bananaTransaction.name }) .timestamp(timestamp) .duration(1000) .success() @@ -107,8 +111,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(bananaTransaction.failureRate) .generator((timestamp) => serviceInstance - .transaction(bananaTransaction.name) - .errors(serviceInstance.error('error 2', 'bar').timestamp(timestamp)) + .transaction({ transactionName: bananaTransaction.name }) + .errors( + serviceInstance.error({ message: 'error 2', type: 'bar' }).timestamp(timestamp) + ) .duration(1000) .timestamp(timestamp) .failure() diff --git a/x-pack/test/apm_api_integration/tests/errors/generate_data.ts b/x-pack/test/apm_api_integration/tests/errors/generate_data.ts index 75d80b85990317..dee5dce49076d5 100644 --- a/x-pack/test/apm_api_integration/tests/errors/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/errors/generate_data.ts @@ -31,7 +31,9 @@ export async function generateData({ start: number; end: number; }) { - const serviceGoProdInstance = apm.service(serviceName, 'production', 'go').instance('instance-a'); + const serviceGoProdInstance = apm + .service({ name: serviceName, environment: 'production', agentName: 'go' }) + .instance('instance-a'); const interval = '1m'; @@ -43,7 +45,7 @@ export async function generateData({ .rate(transaction.successRate) .generator((timestamp) => serviceGoProdInstance - .transaction(transaction.name) + .transaction({ transactionName: transaction.name }) .timestamp(timestamp) .duration(1000) .success() @@ -54,9 +56,11 @@ export async function generateData({ .rate(transaction.failureRate) .generator((timestamp) => serviceGoProdInstance - .transaction(transaction.name) + .transaction({ transactionName: transaction.name }) .errors( - serviceGoProdInstance.error(`Error ${index}`, transaction.name).timestamp(timestamp) + serviceGoProdInstance + .error({ message: `Error ${index}`, type: transaction.name }) + .timestamp(timestamp) ) .duration(1000) .timestamp(timestamp) diff --git a/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/generate_data.ts b/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/generate_data.ts index 6991c66e8eede2..60f0e09875dbc1 100644 --- a/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/generate_data.ts @@ -31,7 +31,9 @@ export async function generateData({ start: number; end: number; }) { - const serviceGoProdInstance = apm.service(serviceName, 'production', 'go').instance('instance-a'); + const serviceGoProdInstance = apm + .service({ name: serviceName, environment: 'production', agentName: 'go' }) + .instance('instance-a'); const interval = '1m'; @@ -43,7 +45,7 @@ export async function generateData({ .rate(transaction.successRate) .generator((timestamp) => serviceGoProdInstance - .transaction(transaction.name) + .transaction({ transactionName: transaction.name }) .timestamp(timestamp) .duration(1000) .success() @@ -54,10 +56,10 @@ export async function generateData({ .rate(transaction.failureRate) .generator((timestamp) => serviceGoProdInstance - .transaction(transaction.name) + .transaction({ transactionName: transaction.name }) .errors( serviceGoProdInstance - .error('Error 1', transaction.name, 'Error test') + .error({ message: 'Error 1', type: transaction.name, groupingName: 'Error test' }) .timestamp(timestamp) ) .duration(1000) diff --git a/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/generate_data.ts b/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/generate_data.ts index 3e37fc871405fa..e377b3c0db995b 100644 --- a/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/generate_data.ts @@ -31,7 +31,9 @@ export async function generateData({ start: number; end: number; }) { - const serviceGoProdInstance = apm.service(serviceName, 'production', 'go').instance('instance-a'); + const serviceGoProdInstance = apm + .service({ name: serviceName, environment: 'production', agentName: 'go' }) + .instance('instance-a'); const interval = '1m'; @@ -43,7 +45,7 @@ export async function generateData({ .rate(transaction.successRate) .generator((timestamp) => serviceGoProdInstance - .transaction(transaction.name) + .transaction({ transactionName: transaction.name }) .timestamp(timestamp) .duration(1000) .success() @@ -54,13 +56,13 @@ export async function generateData({ .rate(transaction.failureRate) .generator((timestamp) => serviceGoProdInstance - .transaction(transaction.name) + .transaction({ transactionName: transaction.name }) .errors( serviceGoProdInstance - .error(`Error 1 transaction ${transaction.name}`) + .error({ message: `Error 1 transaction ${transaction.name}` }) .timestamp(timestamp), serviceGoProdInstance - .error(`Error 2 transaction ${transaction.name}`) + .error({ message: `Error 2 transaction ${transaction.name}` }) .timestamp(timestamp) ) .duration(1000) diff --git a/x-pack/test/apm_api_integration/tests/infrastructure/generate_data.ts b/x-pack/test/apm_api_integration/tests/infrastructure/generate_data.ts index 5f4f80061ed2f2..5bab490b303a2c 100644 --- a/x-pack/test/apm_api_integration/tests/infrastructure/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/infrastructure/generate_data.ts @@ -17,10 +17,12 @@ export async function generateData({ end: number; }) { const serviceRunsInContainerInstance = apm - .service('synth-go', 'production', 'go') + .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) .instance('instance-a'); - const serviceInstance = apm.service('synth-java', 'production', 'java').instance('instance-b'); + const serviceInstance = apm + .service({ name: 'synth-java', environment: 'production', agentName: 'java' }) + .instance('instance-b'); await synthtraceEsClient.index( timerange(start, end) @@ -28,7 +30,7 @@ export async function generateData({ .generator((timestamp) => { return [ serviceRunsInContainerInstance - .transaction('GET /apple 🍎') + .transaction({ transactionName: 'GET /apple 🍎' }) .defaults({ 'container.id': 'foo', 'host.hostname': 'bar', @@ -38,7 +40,7 @@ export async function generateData({ .duration(1000) .success(), serviceInstance - .transaction('GET /banana 🍌') + .transaction({ transactionName: 'GET /banana 🍌' }) .defaults({ 'host.hostname': 'bar', }) diff --git a/x-pack/test/apm_api_integration/tests/latency/service_apis.spec.ts b/x-pack/test/apm_api_integration/tests/latency/service_apis.spec.ts index b38a7ef3052c2c..48ffb06ed3bf2f 100644 --- a/x-pack/test/apm_api_integration/tests/latency/service_apis.spec.ts +++ b/x-pack/test/apm_api_integration/tests/latency/service_apis.spec.ts @@ -124,10 +124,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { const GO_DEV_DURATION = 500; before(async () => { const serviceGoProdInstance = apm - .service(serviceName, 'production', 'go') + .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance('instance-a'); const serviceGoDevInstance = apm - .service(serviceName, 'development', 'go') + .service({ name: serviceName, environment: 'development', agentName: 'go' }) .instance('instance-b'); await synthtraceEsClient.index([ @@ -136,7 +136,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_PROD_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction('GET /api/product/list') + .transaction({ transactionName: 'GET /api/product/list' }) .duration(GO_PROD_DURATION) .timestamp(timestamp) ), @@ -145,7 +145,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_DEV_RATE) .generator((timestamp) => serviceGoDevInstance - .transaction('GET /api/product/:id') + .transaction({ transactionName: 'GET /api/product/:id' }) .duration(GO_DEV_DURATION) .timestamp(timestamp) ), diff --git a/x-pack/test/apm_api_integration/tests/latency/service_maps.spec.ts b/x-pack/test/apm_api_integration/tests/latency/service_maps.spec.ts index e9b9749b659e21..5d1d3e6e62b54b 100644 --- a/x-pack/test/apm_api_integration/tests/latency/service_maps.spec.ts +++ b/x-pack/test/apm_api_integration/tests/latency/service_maps.spec.ts @@ -67,10 +67,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { const GO_DEV_DURATION = 500; before(async () => { const serviceGoProdInstance = apm - .service(serviceName, 'production', 'go') + .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance('instance-a'); const serviceGoDevInstance = apm - .service(serviceName, 'development', 'go') + .service({ name: serviceName, environment: 'development', agentName: 'go' }) .instance('instance-b'); await synthtraceEsClient.index([ @@ -79,7 +79,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_PROD_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction('GET /api/product/list', 'Worker') + .transaction({ + transactionName: 'GET /api/product/list', + transactionType: 'Worker', + }) .duration(GO_PROD_DURATION) .timestamp(timestamp) ), @@ -88,7 +91,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_DEV_RATE) .generator((timestamp) => serviceGoDevInstance - .transaction('GET /api/product/:id') + .transaction({ transactionName: 'GET /api/product/:id' }) .duration(GO_DEV_DURATION) .timestamp(timestamp) ), diff --git a/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.spec.ts b/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.spec.ts index d25b17cc0dc6b3..737efa4634de24 100644 --- a/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.spec.ts +++ b/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.spec.ts @@ -90,14 +90,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { const JAVA_PROD_RATE = 45; before(async () => { const serviceGoProdInstance = apm - .service('synth-go', 'production', 'go') + .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) .instance('instance-a'); const serviceGoDevInstance = apm - .service('synth-go', 'development', 'go') + .service({ name: 'synth-go', environment: 'development', agentName: 'go' }) .instance('instance-b'); const serviceJavaInstance = apm - .service('synth-java', 'production', 'java') + .service({ name: 'synth-java', environment: 'production', agentName: 'java' }) .instance('instance-c'); await synthtraceEsClient.index([ @@ -106,7 +106,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_PROD_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction('GET /api/product/list') + .transaction({ transactionName: 'GET /api/product/list' }) .duration(1000) .timestamp(timestamp) ), @@ -115,7 +115,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_DEV_RATE) .generator((timestamp) => serviceGoDevInstance - .transaction('GET /api/product/:id') + .transaction({ transactionName: 'GET /api/product/:id' }) .duration(1000) .timestamp(timestamp) ), @@ -124,7 +124,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(JAVA_PROD_RATE) .generator((timestamp) => serviceJavaInstance - .transaction('POST /api/product/buy') + .transaction({ transactionName: 'POST /api/product/buy' }) .duration(1000) .timestamp(timestamp) ), 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 index c582c929c67cb4..5aeb0e7db72a4c 100644 --- 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 @@ -50,7 +50,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Service nodes when data is loaded', { config: 'basic', archives: [] }, () => { before(async () => { - const instance = apm.service(serviceName, 'production', 'go').instance(instanceName); + const instance = apm + .service({ name: serviceName, environment: 'production', agentName: 'go' }) + .instance(instanceName); await synthtraceEsClient.index( timerange(start, end) .interval('1m') diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.spec.ts index 8313ec635c69af..80865880cd6c40 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.spec.ts @@ -296,8 +296,16 @@ export default function ApiTest({ getService }: FtrProviderContext) { const rangeEnd = new Date('2021-01-01T12:15:00.000Z').getTime() - 1; before(async () => { - const goService = apm.service('opbeans-go', 'production', 'go'); - const javaService = apm.service('opbeans-java', 'production', 'java'); + const goService = apm.service({ + name: 'opbeans-go', + environment: 'production', + agentName: 'go', + }); + const javaService = apm.service({ + name: 'opbeans-java', + environment: 'production', + agentName: 'java', + }); const goInstanceA = goService.instance('go-instance-a'); const goInstanceB = goService.instance('go-instance-b'); @@ -310,7 +318,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { function withSpans(timestamp: number) { return new Array(3).fill(undefined).map(() => goInstanceA - .span('GET apm-*/_search', 'db', 'elasticsearch') + .span({ + spanName: 'GET apm-*/_search', + spanType: 'db', + spanSubtype: 'elasticsearch', + }) .timestamp(timestamp + 100) .duration(300) .destination('elasticsearch') @@ -321,7 +333,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { return synthtraceEsClient.index([ interval.rate(GO_A_INSTANCE_RATE_SUCCESS).generator((timestamp) => goInstanceA - .transaction('GET /api/product/list') + .transaction({ transactionName: 'GET /api/product/list' }) .success() .duration(500) .timestamp(timestamp) @@ -329,7 +341,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ), interval.rate(GO_A_INSTANCE_RATE_FAILURE).generator((timestamp) => goInstanceA - .transaction('GET /api/product/list') + .transaction({ transactionName: 'GET /api/product/list' }) .failure() .duration(500) .timestamp(timestamp) @@ -337,7 +349,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ), interval.rate(GO_B_INSTANCE_RATE_SUCCESS).generator((timestamp) => goInstanceB - .transaction('GET /api/product/list') + .transaction({ transactionName: 'GET /api/product/list' }) .success() .duration(500) .timestamp(timestamp) @@ -345,7 +357,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ), interval.rate(JAVA_INSTANCE_RATE).generator((timestamp) => javaInstance - .transaction('GET /api/product/list') + .transaction({ transactionName: 'GET /api/product/list' }) .success() .duration(500) .timestamp(timestamp) diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/generate_data.ts b/x-pack/test/apm_api_integration/tests/services/error_groups/generate_data.ts index 2999b2f68c29eb..a93ac5420a785f 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/services/error_groups/generate_data.ts @@ -27,7 +27,9 @@ export async function generateData({ start: number; end: number; }) { - const serviceGoProdInstance = apm.service(serviceName, 'production', 'go').instance('instance-a'); + const serviceGoProdInstance = apm + .service({ name: serviceName, environment: 'production', agentName: 'go' }) + .instance('instance-a'); const transactionNameProductList = 'GET /api/product/list'; const transactionNameProductId = 'GET /api/product/:id'; @@ -47,7 +49,7 @@ export async function generateData({ .rate(PROD_LIST_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction(transactionNameProductList) + .transaction({ transactionName: transactionNameProductList }) .timestamp(timestamp) .duration(1000) .success() @@ -57,8 +59,10 @@ export async function generateData({ .rate(PROD_LIST_ERROR_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction(transactionNameProductList) - .errors(serviceGoProdInstance.error(ERROR_NAME_1, 'foo').timestamp(timestamp)) + .transaction({ transactionName: transactionNameProductList }) + .errors( + serviceGoProdInstance.error({ message: ERROR_NAME_1, type: 'foo' }).timestamp(timestamp) + ) .duration(1000) .timestamp(timestamp) .failure() @@ -68,7 +72,7 @@ export async function generateData({ .rate(PROD_ID_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction(transactionNameProductId) + .transaction({ transactionName: transactionNameProductId }) .timestamp(timestamp) .duration(1000) .success() @@ -78,8 +82,10 @@ export async function generateData({ .rate(PROD_ID_ERROR_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction(transactionNameProductId) - .errors(serviceGoProdInstance.error(ERROR_NAME_2, 'bar').timestamp(timestamp)) + .transaction({ transactionName: transactionNameProductId }) + .errors( + serviceGoProdInstance.error({ message: ERROR_NAME_2, type: 'bar' }).timestamp(timestamp) + ) .duration(1000) .timestamp(timestamp) .failure() diff --git a/x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts b/x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts index 49160f9b5caf2d..faa160063dfa16 100644 --- a/x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts @@ -57,7 +57,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'basic', archives: [] }, () => { before(async () => { - const instance = apm.service(serviceName, 'production', 'go').instance(instanceName); + const instance = apm + .service({ name: serviceName, environment: 'production', agentName: 'go' }) + .instance(instanceName); await synthtraceEsClient.index( timerange(start, end) .interval('1m') @@ -65,7 +67,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .generator((timestamp) => instance .containerId(instanceName) - .transaction('GET /api/product/list') + .transaction({ transactionName: 'GET /api/product/list' }) .timestamp(timestamp) .duration(1000) .success() diff --git a/x-pack/test/apm_api_integration/tests/services/service_details/generate_data.ts b/x-pack/test/apm_api_integration/tests/services/service_details/generate_data.ts index 4765f52b855d3a..22e8e12c668178 100644 --- a/x-pack/test/apm_api_integration/tests/services/service_details/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/services/service_details/generate_data.ts @@ -65,7 +65,9 @@ export async function generateData({ const { name: serviceRunTimeName, version: serviceRunTimeVersion } = runtime; const { name: agentName, version: agentVersion } = agent; - const instance = apm.service(serviceName, 'production', agentName).instance('instance-a'); + const instance = apm + .service({ name: serviceName, environment: 'production', agentName }) + .instance('instance-a'); const traceEvents = [ timerange(start, end) @@ -74,7 +76,7 @@ export async function generateData({ .generator((timestamp) => instance .containerId('instance-a') - .transaction(transaction.name) + .transaction({ transactionName: transaction.name }) .timestamp(timestamp) .defaults({ 'cloud.provider': provider, @@ -101,7 +103,7 @@ export async function generateData({ .rate(rate) .generator((timestamp) => instance - .transaction(transaction.name) + .transaction({ transactionName: transaction.name }) .timestamp(timestamp) .defaults({ 'cloud.provider': provider, diff --git a/x-pack/test/apm_api_integration/tests/services/service_icons/generate_data.ts b/x-pack/test/apm_api_integration/tests/services/service_icons/generate_data.ts index 05f63f5ac05af5..36b0e30f92efd6 100644 --- a/x-pack/test/apm_api_integration/tests/services/service_icons/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/services/service_icons/generate_data.ts @@ -33,14 +33,16 @@ export async function generateData({ const { serviceName, agentName, rate, cloud, transaction } = dataConfig; const { provider, serviceName: cloudServiceName } = cloud; - const instance = apm.service(serviceName, 'production', agentName).instance('instance-a'); + const instance = apm + .service({ name: serviceName, environment: 'production', agentName }) + .instance('instance-a'); const traceEvents = timerange(start, end) .interval('30s') .rate(rate) .generator((timestamp) => instance - .transaction(transaction.name) + .transaction({ transactionName: transaction.name }) .defaults({ 'kubernetes.pod.uid': 'test', 'cloud.provider': provider, diff --git a/x-pack/test/apm_api_integration/tests/services/sorted_and_filtered_services.spec.ts b/x-pack/test/apm_api_integration/tests/services/sorted_and_filtered_services.spec.ts index ca2cb0c3d20a71..762722a77be79a 100644 --- a/x-pack/test/apm_api_integration/tests/services/sorted_and_filtered_services.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/sorted_and_filtered_services.spec.ts @@ -53,11 +53,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { // FLAKY: https://github.com/elastic/kibana/issues/127939 registry.when.skip('Sorted and filtered services', { config: 'trial', archives: [] }, () => { before(async () => { - const serviceA = apm.service(SERVICE_NAME_PREFIX + 'a', 'production', 'java').instance('a'); + const serviceA = apm + .service({ name: SERVICE_NAME_PREFIX + 'a', environment: 'production', agentName: 'java' }) + .instance('a'); - const serviceB = apm.service(SERVICE_NAME_PREFIX + 'b', 'development', 'go').instance('b'); + const serviceB = apm + .service({ name: SERVICE_NAME_PREFIX + 'b', environment: 'development', agentName: 'go' }) + .instance('b'); - const serviceC = apm.service(SERVICE_NAME_PREFIX + 'c', 'development', 'go').instance('c'); + const serviceC = apm + .service({ name: SERVICE_NAME_PREFIX + 'c', environment: 'development', agentName: 'go' }) + .instance('c'); const spikeStart = new Date('2021-01-07T12:00:00.000Z').getTime(); const spikeEnd = new Date('2021-01-07T14:00:00.000Z').getTime(); @@ -69,11 +75,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { const isInSpike = spikeStart <= timestamp && spikeEnd >= timestamp; return [ serviceA - .transaction('GET /api') + .transaction({ transactionName: 'GET /api' }) .duration(isInSpike ? 1000 : 1100) .timestamp(timestamp), serviceB - .transaction('GET /api') + .transaction({ transactionName: 'GET /api' }) .duration(isInSpike ? 1000 : 4000) .timestamp(timestamp), ]; @@ -86,7 +92,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { .interval('15m') .rate(1) .generator((timestamp) => { - return serviceC.transaction('GET /api', 'custom').duration(1000).timestamp(timestamp); + return serviceC + .transaction({ transactionName: 'GET /api', transactionType: 'custom' }) + .duration(1000) + .timestamp(timestamp); }); await synthtraceClient.index(eventsWithinTimerange.merge(eventsOutsideOfTimerange)); diff --git a/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts b/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts index c9c95b2e99bbc5..ffd6d43bec959a 100644 --- a/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts @@ -71,14 +71,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { before(async () => { const serviceGoProdInstance = apm - .service(serviceName, 'production', 'go') + .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance('instance-a'); const serviceGoDevInstance = apm - .service(serviceName, 'development', 'go') + .service({ name: serviceName, environment: 'development', agentName: 'go' }) .instance('instance-b'); const serviceJavaInstance = apm - .service('synth-java', 'development', 'java') + .service({ name: 'synth-java', environment: 'development', agentName: 'java' }) .instance('instance-c'); await synthtraceEsClient.index([ @@ -87,7 +87,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_PROD_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction('GET /api/product/list') + .transaction({ transactionName: 'GET /api/product/list' }) .duration(1000) .timestamp(timestamp) ), @@ -96,7 +96,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_DEV_RATE) .generator((timestamp) => serviceGoDevInstance - .transaction('GET /api/product/:id') + .transaction({ transactionName: 'GET /api/product/:id' }) .duration(1000) .timestamp(timestamp) ), @@ -105,7 +105,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(JAVA_PROD_RATE) .generator((timestamp) => serviceJavaInstance - .transaction('POST /api/product/buy') + .transaction({ transactionName: 'POST /api/product/buy' }) .duration(1000) .timestamp(timestamp) ), diff --git a/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts b/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts index 898f12ceaeffb2..c8b21729484bc3 100644 --- a/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts @@ -71,19 +71,19 @@ export default function ApiTest({ getService }: FtrProviderContext) { const errorInterval = range.interval('5s'); const multipleEnvServiceProdInstance = apm - .service('multiple-env-service', 'production', 'go') + .service({ name: 'multiple-env-service', environment: 'production', agentName: 'go' }) .instance('multiple-env-service-production'); const multipleEnvServiceDevInstance = apm - .service('multiple-env-service', 'development', 'go') + .service({ name: 'multiple-env-service', environment: 'development', agentName: 'go' }) .instance('multiple-env-service-development'); const metricOnlyInstance = apm - .service('metric-only-service', 'production', 'java') + .service({ name: 'metric-only-service', environment: 'production', agentName: 'java' }) .instance('metric-only-production'); const errorOnlyInstance = apm - .service('error-only-service', 'production', 'java') + .service({ name: 'error-only-service', environment: 'production', agentName: 'java' }) .instance('error-only-production'); const config = { @@ -105,7 +105,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(config.multiple.prod.rps) .generator((timestamp) => multipleEnvServiceProdInstance - .transaction('GET /api') + .transaction({ transactionName: 'GET /api' }) .timestamp(timestamp) .duration(config.multiple.prod.duration) .success() @@ -114,7 +114,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(config.multiple.dev.rps) .generator((timestamp) => multipleEnvServiceDevInstance - .transaction('GET /api') + .transaction({ transactionName: 'GET /api' }) .timestamp(timestamp) .duration(config.multiple.dev.duration) .failure() @@ -123,7 +123,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(config.multiple.prod.rps) .generator((timestamp) => multipleEnvServiceDevInstance - .transaction('non-request', 'rpc') + .transaction({ transactionName: 'non-request', transactionType: 'rpc' }) .timestamp(timestamp) .duration(config.multiple.prod.duration) .success() @@ -140,7 +140,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { ), errorInterval .rate(1) - .generator((timestamp) => errorOnlyInstance.error('Foo').timestamp(timestamp)), + .generator((timestamp) => + errorOnlyInstance.error({ message: 'Foo' }).timestamp(timestamp) + ), ]); }); diff --git a/x-pack/test/apm_api_integration/tests/span_links/data_generator.ts b/x-pack/test/apm_api_integration/tests/span_links/data_generator.ts index 37bd72ff71c599..0af23ab0dc736b 100644 --- a/x-pack/test/apm_api_integration/tests/span_links/data_generator.ts +++ b/x-pack/test/apm_api_integration/tests/span_links/data_generator.ts @@ -10,7 +10,7 @@ import uuid from 'uuid'; function getProducerInternalOnly() { const producerInternalOnlyInstance = apm - .service('producer-internal-only', 'production', 'go') + .service({ name: 'producer-internal-only', environment: 'production', agentName: 'go' }) .instance('instance a'); const events = timerange( @@ -21,13 +21,13 @@ function getProducerInternalOnly() { .rate(1) .generator((timestamp) => { return producerInternalOnlyInstance - .transaction(`Transaction A`) + .transaction({ transactionName: `Transaction A` }) .timestamp(timestamp) .duration(1000) .success() .children( producerInternalOnlyInstance - .span(`Span A`, 'external', 'http') + .span({ spanName: `Span A`, spanType: 'external', spanSubtype: 'http' }) .timestamp(timestamp + 50) .duration(100) .success() @@ -57,7 +57,7 @@ function getProducerInternalOnly() { function getProducerExternalOnly() { const producerExternalOnlyInstance = apm - .service('producer-external-only', 'production', 'java') + .service({ name: 'producer-external-only', environment: 'production', agentName: 'java' }) .instance('instance b'); const events = timerange( @@ -68,13 +68,13 @@ function getProducerExternalOnly() { .rate(1) .generator((timestamp) => { return producerExternalOnlyInstance - .transaction(`Transaction B`) + .transaction({ transactionName: `Transaction B` }) .timestamp(timestamp) .duration(1000) .success() .children( producerExternalOnlyInstance - .span(`Span B`, 'external', 'http') + .span({ spanName: `Span B`, spanType: 'external', spanSubtype: 'http' }) .defaults({ 'span.links': [{ trace: { id: 'trace#1' }, span: { id: 'span#1' } }], }) @@ -82,7 +82,7 @@ function getProducerExternalOnly() { .duration(100) .success(), producerExternalOnlyInstance - .span(`Span B.1`, 'external', 'http') + .span({ spanName: `Span B.1`, spanType: 'external', spanSubtype: 'http' }) .timestamp(timestamp + 50) .duration(100) .success() @@ -130,7 +130,7 @@ function getProducerConsumer({ const externalTraceId = uuid.v4(); const producerConsumerInstance = apm - .service('producer-consumer', 'production', 'ruby') + .service({ name: 'producer-consumer', environment: 'production', agentName: 'ruby' }) .instance('instance c'); const events = timerange( @@ -141,7 +141,7 @@ function getProducerConsumer({ .rate(1) .generator((timestamp) => { return producerConsumerInstance - .transaction(`Transaction C`) + .transaction({ transactionName: `Transaction C` }) .defaults({ 'span.links': [ producerInternalOnlySpanASpanLink, @@ -154,7 +154,7 @@ function getProducerConsumer({ .success() .children( producerConsumerInstance - .span(`Span C`, 'external', 'http') + .span({ spanName: `Span C`, spanType: 'external', spanSubtype: 'http' }) .timestamp(timestamp + 50) .duration(100) .success() @@ -200,7 +200,7 @@ function getConsumerMultiple({ producerConsumerTransactionCLink: SpanLink; }) { const consumerMultipleInstance = apm - .service('consumer-multiple', 'production', 'nodejs') + .service({ name: 'consumer-multiple', environment: 'production', agentName: 'nodejs' }) .instance('instance d'); const events = timerange( @@ -211,14 +211,14 @@ function getConsumerMultiple({ .rate(1) .generator((timestamp) => { return consumerMultipleInstance - .transaction(`Transaction D`) + .transaction({ transactionName: `Transaction D` }) .defaults({ 'span.links': [producerInternalOnlySpanALink, producerConsumerSpanCLink] }) .timestamp(timestamp) .duration(1000) .success() .children( consumerMultipleInstance - .span(`Span E`, 'external', 'http') + .span({ spanName: `Span E`, spanType: 'external', spanSubtype: 'http' }) .defaults({ 'span.links': [producerExternalOnlySpanBLink, producerConsumerTransactionCLink], }) diff --git a/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer_timeseries_chart.spec.ts b/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer_timeseries_chart.spec.ts index fe0034c23e0bab..632e17c37509a5 100644 --- a/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer_timeseries_chart.spec.ts +++ b/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer_timeseries_chart.spec.ts @@ -56,21 +56,31 @@ export default function ApiTest({ getService }: FtrProviderContext) { let status: number; before(async () => { - const serviceGo1 = apm.service('synth-go-1', 'production', 'go').instance('instance'); - const serviceGo2 = apm.service('synth-go-2', 'production', 'go').instance('instance'); + const serviceGo1 = apm + .service({ name: 'synth-go-1', environment: 'production', agentName: 'go' }) + .instance('instance'); + const serviceGo2 = apm + .service({ name: 'synth-go-2', environment: 'production', agentName: 'go' }) + .instance('instance'); await synthtraceEsClient.index([ timerange(start, end) .interval('5m') .rate(1) .generator((timestamp) => - serviceGo1.transaction('GET /api/product/list1').duration(2000).timestamp(timestamp) + serviceGo1 + .transaction({ transactionName: 'GET /api/product/list1' }) + .duration(2000) + .timestamp(timestamp) ), timerange(start, end) .interval('5m') .rate(1) .generator((timestamp) => - serviceGo2.transaction('GET /api/product/list2').duration(2000).timestamp(timestamp) + serviceGo2 + .transaction({ transactionName: 'GET /api/product/list2' }) + .duration(2000) + .timestamp(timestamp) ), ]); diff --git a/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts b/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts index c990a7c632caa9..926724156d553e 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts +++ b/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts @@ -99,10 +99,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { const JAVA_PROD_RATE = 25; before(async () => { const serviceGoProdInstance = apm - .service('synth-go', 'production', 'go') + .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) .instance('instance-a'); const serviceJavaInstance = apm - .service('synth-java', 'development', 'java') + .service({ name: 'synth-java', environment: 'development', agentName: 'java' }) .instance('instance-c'); await synthtraceEsClient.index([ @@ -111,22 +111,30 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_PROD_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction('GET /api/product/list') + .transaction({ transactionName: 'GET /api/product/list' }) .duration(1000) .timestamp(timestamp) .children( serviceGoProdInstance - .span('GET apm-*/_search', 'db', 'elasticsearch') + .span({ + spanName: 'GET apm-*/_search', + spanType: 'db', + spanSubtype: 'elasticsearch', + }) .duration(1000) .success() .destination('elasticsearch') .timestamp(timestamp), serviceGoProdInstance - .span('custom_operation', 'app') + .span({ spanName: 'custom_operation', spanType: 'app' }) .duration(550) .children( serviceGoProdInstance - .span('SELECT FROM products', 'db', 'postgresql') + .span({ + spanName: 'SELECT FROM products', + spanType: 'db', + spanSubtype: 'postgresql', + }) .duration(500) .success() .destination('postgresql') @@ -141,18 +149,22 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(JAVA_PROD_RATE) .generator((timestamp) => serviceJavaInstance - .transaction('POST /api/product/buy') + .transaction({ transactionName: 'POST /api/product/buy' }) .duration(1000) .timestamp(timestamp) .children( serviceJavaInstance - .span('GET apm-*/_search', 'db', 'elasticsearch') + .span({ + spanName: 'GET apm-*/_search', + spanType: 'db', + spanSubtype: 'elasticsearch', + }) .duration(1000) .success() .destination('elasticsearch') .timestamp(timestamp), serviceJavaInstance - .span('custom_operation', 'app') + .span({ spanName: 'custom_operation', spanType: 'app' }) .duration(50) .success() .timestamp(timestamp) diff --git a/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts b/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts index ef091dc83a429e..d4d843e8cc6639 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts +++ b/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts @@ -110,10 +110,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { const GO_DEV_RATE = 20; before(async () => { const serviceGoProdInstance = apm - .service(serviceName, 'production', 'go') + .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance('instance-a'); const serviceGoDevInstance = apm - .service(serviceName, 'development', 'go') + .service({ name: serviceName, environment: 'development', agentName: 'go' }) .instance('instance-b'); await synthtraceEsClient.index([ @@ -122,7 +122,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_PROD_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction('GET /api/product/list') + .transaction({ transactionName: 'GET /api/product/list' }) .duration(1000) .timestamp(timestamp) ), @@ -131,7 +131,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_DEV_RATE) .generator((timestamp) => serviceGoDevInstance - .transaction('GET /api/product/:id') + .transaction({ transactionName: 'GET /api/product/:id' }) .duration(1000) .timestamp(timestamp) ), diff --git a/x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts b/x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts index fd775ec9af2a9e..039a4f0f548b02 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts +++ b/x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts @@ -75,10 +75,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { const GO_DEV_RATE = 20; before(async () => { const serviceGoProdInstance = apm - .service(serviceName, 'production', 'go') + .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance('instance-a'); const serviceGoDevInstance = apm - .service(serviceName, 'development', 'go') + .service({ name: serviceName, environment: 'development', agentName: 'go' }) .instance('instance-b'); await synthtraceEsClient.index([ @@ -87,7 +87,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_PROD_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction('GET /apple 🍎 ', 'Worker') + .transaction({ transactionName: 'GET /apple 🍎 ', transactionType: 'Worker' }) .duration(1000) .timestamp(timestamp) ), @@ -95,7 +95,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { .interval('1m') .rate(GO_DEV_RATE) .generator((timestamp) => - serviceGoDevInstance.transaction('GET /apple 🍎 ').duration(1000).timestamp(timestamp) + serviceGoDevInstance + .transaction({ transactionName: 'GET /apple 🍎 ' }) + .duration(1000) + .timestamp(timestamp) ), ]); }); diff --git a/x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts b/x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts index 0c0696f801a952..a2a44e7d086da1 100644 --- a/x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts +++ b/x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts @@ -97,11 +97,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Find traces when traces exist', { config: 'basic', archives: [] }, () => { before(() => { - const java = apm.service('java', 'production', 'java').instance('java'); + const java = apm + .service({ name: 'java', environment: 'production', agentName: 'java' }) + .instance('java'); - const node = apm.service('node', 'development', 'nodejs').instance('node'); + const node = apm + .service({ name: 'node', environment: 'development', agentName: 'nodejs' }) + .instance('node'); - const python = apm.service('python', 'production', 'python').instance('python'); + const python = apm + .service({ name: 'python', environment: 'production', agentName: 'python' }) + .instance('python'); function generateTrace(timestamp: number, order: Instance[], db?: 'elasticsearch' | 'redis') { return order @@ -114,7 +120,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const time = timestamp + invertedIndex * 10; const transaction: Transaction = instance - .transaction(`GET /${instance.fields['service.name']!}/api`) + .transaction({ transactionName: `GET /${instance.fields['service.name']!}/api` }) .timestamp(time) .duration(duration); @@ -122,7 +128,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const next = order[invertedIndex + 1].fields['service.name']!; transaction.children( instance - .span(`GET ${next}/api`, 'external', 'http') + .span({ spanName: `GET ${next}/api`, spanType: 'external', spanSubtype: 'http' }) .destination(next) .duration(duration) .timestamp(time + 1) @@ -131,7 +137,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { } else if (db) { transaction.children( instance - .span(db, 'db', db) + .span({ spanName: db, spanType: 'db', spanSubtype: db }) .destination(db) .duration(duration) .timestamp(time + 1) diff --git a/x-pack/test/apm_api_integration/tests/traces/trace_by_id.spec.ts b/x-pack/test/apm_api_integration/tests/traces/trace_by_id.spec.ts index 509d70caf52916..087ec58e83806a 100644 --- a/x-pack/test/apm_api_integration/tests/traces/trace_by_id.spec.ts +++ b/x-pack/test/apm_api_integration/tests/traces/trace_by_id.spec.ts @@ -55,25 +55,31 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Trace exists', { config: 'basic', archives: [] }, () => { let serviceATraceId: string; before(async () => { - const instanceJava = apm.service('synth-apple', 'production', 'java').instance('instance-b'); + const instanceJava = apm + .service({ name: 'synth-apple', environment: 'production', agentName: 'java' }) + .instance('instance-b'); const events = timerange(start, end) .interval('1m') .rate(1) .generator((timestamp) => { return [ instanceJava - .transaction('GET /apple 🍏') + .transaction({ transactionName: 'GET /apple 🍏' }) .timestamp(timestamp) .duration(1000) .failure() .errors( instanceJava - .error('[ResponseError] index_not_found_exception') + .error({ message: '[ResponseError] index_not_found_exception' }) .timestamp(timestamp + 50) ) .children( instanceJava - .span('get_green_apple_🍏', 'db', 'elasticsearch') + .span({ + spanName: 'get_green_apple_🍏', + spanType: 'db', + spanSubtype: 'elasticsearch', + }) .timestamp(timestamp + 50) .duration(900) .success() diff --git a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.spec.ts index 350830abcbba3a..7215dd933c87e8 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.spec.ts @@ -81,7 +81,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const GO_PROD_ERROR_RATE = 25; before(async () => { const serviceGoProdInstance = apm - .service(serviceName, 'production', 'go') + .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance('instance-a'); const transactionName = 'GET /api/product/list'; @@ -92,7 +92,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_PROD_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction(transactionName) + .transaction({ transactionName }) .timestamp(timestamp) .duration(1000) .success() @@ -102,7 +102,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .rate(GO_PROD_ERROR_RATE) .generator((timestamp) => serviceGoProdInstance - .transaction(transactionName) + .transaction({ transactionName }) .duration(1000) .timestamp(timestamp) .failure() diff --git a/x-pack/test/cases_api_integration/common/fixtures/plugins/observability/server/plugin.ts b/x-pack/test/cases_api_integration/common/fixtures/plugins/observability/server/plugin.ts index 9dcdc8a26af0e6..fb872f775c5bb6 100644 --- a/x-pack/test/cases_api_integration/common/fixtures/plugins/observability/server/plugin.ts +++ b/x-pack/test/cases_api_integration/common/fixtures/plugins/observability/server/plugin.ts @@ -31,7 +31,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu cases: ['observabilityFixture'], privileges: { all: { - api: ['casesSuggestUserProfiles'], + api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles'], app: ['kibana'], cases: { all: ['observabilityFixture'], @@ -43,6 +43,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu ui: [], }, read: { + api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles'], app: ['kibana'], cases: { read: ['observabilityFixture'], diff --git a/x-pack/test/cases_api_integration/common/fixtures/plugins/security_solution/server/plugin.ts b/x-pack/test/cases_api_integration/common/fixtures/plugins/security_solution/server/plugin.ts index 36917706d719ce..b22674d66db4cc 100644 --- a/x-pack/test/cases_api_integration/common/fixtures/plugins/security_solution/server/plugin.ts +++ b/x-pack/test/cases_api_integration/common/fixtures/plugins/security_solution/server/plugin.ts @@ -54,7 +54,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu ui: [], }, read: { - api: ['bulkGetUserProfiles'], + api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles'], app: ['kibana'], cases: { read: ['securitySolutionFixture'], diff --git a/x-pack/test/cases_api_integration/common/lib/authentication/index.ts b/x-pack/test/cases_api_integration/common/lib/authentication/index.ts index 5ca8ac3bcd9f74..65e82a2e4fbf39 100644 --- a/x-pack/test/cases_api_integration/common/lib/authentication/index.ts +++ b/x-pack/test/cases_api_integration/common/lib/authentication/index.ts @@ -10,7 +10,7 @@ import { Role, User, UserInfo } from './types'; import { obsOnly, secOnly, secOnlyNoDelete, secOnlyRead, users } from './users'; import { roles } from './roles'; import { spaces } from './spaces'; -import { loginUsers } from '../utils'; +import { loginUsers } from '../user_profiles'; export const getUserInfo = (user: User): UserInfo => ({ username: user.username, diff --git a/x-pack/test/cases_api_integration/common/lib/user_profiles.ts b/x-pack/test/cases_api_integration/common/lib/user_profiles.ts index e68de4418c9bfd..aefe3d0b1c8732 100644 --- a/x-pack/test/cases_api_integration/common/lib/user_profiles.ts +++ b/x-pack/test/cases_api_integration/common/lib/user_profiles.ts @@ -8,6 +8,9 @@ import type SuperTest from 'supertest'; import { UserProfileBulkGetParams, UserProfileServiceStart } from '@kbn/security-plugin/server'; +import { INTERNAL_SUGGEST_USER_PROFILES_URL } from '@kbn/cases-plugin/common/constants'; +import { SuggestUserProfilesRequest } from '@kbn/cases-plugin/common/api'; +import { UserProfileService } from '@kbn/cases-plugin/server/services'; import { superUser } from './authentication/users'; import { User } from './authentication/types'; import { getSpaceUrlPrefix } from './utils'; @@ -37,3 +40,45 @@ export const bulkGetUserProfiles = async ({ return profiles; }; + +export const suggestUserProfiles = async ({ + supertest, + req, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: SuperTest.SuperTest<SuperTest.Test>; + req: SuggestUserProfilesRequest; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): ReturnType<UserProfileService['suggest']> => { + const { body: profiles } = await supertest + .post(`${getSpaceUrlPrefix(auth.space)}${INTERNAL_SUGGEST_USER_PROFILES_URL}`) + .auth(auth.user.username, auth.user.password) + .set('kbn-xsrf', 'true') + .send(req) + .expect(expectedHttpCode); + + return profiles; +}; + +export const loginUsers = async ({ + supertest, + users = [superUser], +}: { + supertest: SuperTest.SuperTest<SuperTest.Test>; + users?: User[]; +}) => { + for (const user of users) { + await supertest + .post('/internal/security/login') + .set('kbn-xsrf', 'xxx') + .send({ + providerType: 'basic', + providerName: 'basic', + currentURL: '/', + params: { username: user.username, password: user.password }, + }) + .expect(200); + } +}; diff --git a/x-pack/test/cases_api_integration/common/lib/utils.ts b/x-pack/test/cases_api_integration/common/lib/utils.ts index 85350fb43f1f27..1c801341a0e998 100644 --- a/x-pack/test/cases_api_integration/common/lib/utils.ts +++ b/x-pack/test/cases_api_integration/common/lib/utils.ts @@ -24,7 +24,6 @@ import { CASE_REPORTERS_URL, CASE_STATUS_URL, CASE_TAGS_URL, - INTERNAL_SUGGEST_USER_PROFILES_URL, } from '@kbn/cases-plugin/common/constants'; import { CasesConfigureRequest, @@ -54,7 +53,6 @@ import { BulkCreateCommentRequest, CommentType, CasesMetricsResponse, - SuggestUserProfilesRequest, } from '@kbn/cases-plugin/common/api'; import { getCaseUserActionUrl } from '@kbn/cases-plugin/common/api/helpers'; import { SignalHit } from '@kbn/security-solution-plugin/server/lib/detection_engine/signals/types'; @@ -62,7 +60,6 @@ import { ActionResult, FindActionResult } from '@kbn/actions-plugin/server/types import { ESCasesConfigureAttributes } from '@kbn/cases-plugin/server/services/configure/types'; import { ESCaseAttributes } from '@kbn/cases-plugin/server/services/cases/types'; import type { SavedObjectsRawDocSource } from '@kbn/core/server'; -import { UserProfileService } from '@kbn/cases-plugin/server/services'; import { User } from './authentication/types'; import { superUser } from './authentication/users'; import { getPostCaseRequest, postCaseReq } from './mock'; @@ -1348,45 +1345,3 @@ export const getReferenceFromEsResponse = ( esResponse: TransportResult<GetResponse<SavedObjectsRawDocSource>, unknown>, id: string ) => esResponse.body._source?.references?.find((r) => r.id === id); - -export const suggestUserProfiles = async ({ - supertest, - req, - expectedHttpCode = 200, - auth = { user: superUser, space: null }, -}: { - supertest: SuperTest.SuperTest<SuperTest.Test>; - req: SuggestUserProfilesRequest; - expectedHttpCode?: number; - auth?: { user: User; space: string | null }; -}): ReturnType<UserProfileService['suggest']> => { - const { body: profiles } = await supertest - .post(`${getSpaceUrlPrefix(auth.space)}${INTERNAL_SUGGEST_USER_PROFILES_URL}`) - .auth(auth.user.username, auth.user.password) - .set('kbn-xsrf', 'true') - .send(req) - .expect(expectedHttpCode); - - return profiles; -}; - -export const loginUsers = async ({ - supertest, - users = [superUser], -}: { - supertest: SuperTest.SuperTest<SuperTest.Test>; - users?: User[]; -}) => { - for (const user of users) { - await supertest - .post('/internal/security/login') - .set('kbn-xsrf', 'xxx') - .send({ - providerType: 'basic', - providerName: 'basic', - currentURL: '/', - params: { username: user.username, password: user.password }, - }) - .expect(200); - } -}; diff --git a/x-pack/test/cases_api_integration/common/lib/validation.ts b/x-pack/test/cases_api_integration/common/lib/validation.ts index c901631388f11b..a84c733586ec06 100644 --- a/x-pack/test/cases_api_integration/common/lib/validation.ts +++ b/x-pack/test/cases_api_integration/common/lib/validation.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { CaseResponse, CasesByAlertId } from '@kbn/cases-plugin/common/api'; +import { xorWith, isEqual } from 'lodash'; /** * Ensure that the result of the alerts API request matches with the cases created for the test. @@ -29,13 +30,12 @@ export function validateCasesFromAlertIDResponse( * Compares two arrays to determine if they are sort of equal. This function returns true if the arrays contain the same * elements but the ordering does not matter. */ -export function arraysToEqual(array1?: object[], array2?: object[]) { +export function arraysToEqual<T>(array1?: T[], array2?: T[]) { if (!array1 || !array2 || array1.length !== array2.length) { return false; } - const array1AsSet = new Set(array1); - return array2.every((item) => array1AsSet.has(item)); + return xorWith(array1, array2, isEqual).length === 0; } /** diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/assignees.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/assignees.ts index b92ddadc32c080..fbe2672c17cbef 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/assignees.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/assignees.ts @@ -10,13 +10,14 @@ import expect from '@kbn/expect'; import { findCasesResp, getPostCaseRequest, postCaseReq } from '../../../../common/lib/mock'; import { createCase, - suggestUserProfiles, getCase, findCases, updateCase, deleteAllCaseItems, } from '../../../../common/lib/utils'; +import { suggestUserProfiles } from '../../../../common/lib/user_profiles'; + import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { bulkGetUserProfiles } from '../../../../common/lib/user_profiles'; import { superUser } from '../../../../common/lib/authentication/users'; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts index 65f3a36cbe4877..3daa29c02b107b 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts @@ -238,9 +238,11 @@ export default ({ getService }: FtrProviderContext): void => { it('returns the correct fields', async () => { const postedCase = await createCase(supertest, postCaseReq); + // all fields that contain the UserRT definition must be included here (aka created_by, closed_by, and updated_by) + // see https://github.com/elastic/kibana/issues/139503 const queryFields: Array<keyof CaseResponse | Array<keyof CaseResponse>> = [ - 'title', - ['title', 'description'], + ['title', 'created_by', 'closed_by', 'updated_by'], + ['title', 'description', 'created_by', 'closed_by', 'updated_by'], ]; for (const fields of queryFields) { diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/suggest_user_profiles.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/suggest_user_profiles.ts index 177d5ffc06486d..f5feab6f045570 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/suggest_user_profiles.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/suggest_user_profiles.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { loginUsers, suggestUserProfiles } from '../../../../common/lib/utils'; +import { loginUsers, suggestUserProfiles } from '../../../../common/lib/user_profiles'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { superUser, @@ -21,6 +21,19 @@ export default function ({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('suggest_user_profiles', () => { + it('returns no suggestions when the owner is an empty array', async () => { + const profiles = await suggestUserProfiles({ + supertest: supertestWithoutAuth, + req: { + name: 'delete', + owners: [], + }, + auth: { user: superUser, space: 'space1' }, + }); + + expect(profiles.length).to.be(0); + }); + it('finds the profile for the user without deletion privileges', async () => { const profiles = await suggestUserProfiles({ supertest: supertestWithoutAuth, diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics_alerts.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics_alerts.ts index 1f58d4b72cea87..060b7dda22dea5 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics_alerts.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics_alerts.ts @@ -51,7 +51,7 @@ export default ({ getService }: FtrProviderContext): void => { { id: '7eb51035-5582-4cb8-9db2-5e71ef09aa5c', name: 'Host-123', count: 2 }, { id: '7eb51035-5582-4cb8-9db2-5e71ef09aa5d', name: 'Host-100', count: 2 }, ]) - ); + ).to.be(true); }); it('returns the user metrics', async () => { @@ -69,7 +69,7 @@ export default ({ getService }: FtrProviderContext): void => { { name: '7bgwxrbmcu', count: 1 }, { name: 'jf9e87gsut', count: 1 }, ]) - ); + ).to.be(true); }); it('returns both the host and user metrics', async () => { @@ -86,7 +86,7 @@ export default ({ getService }: FtrProviderContext): void => { { id: '7eb51035-5582-4cb8-9db2-5e71ef09aa5c', name: 'Host-123', count: 2 }, { id: '7eb51035-5582-4cb8-9db2-5e71ef09aa5d', name: 'Host-100', count: 2 }, ]) - ); + ).to.be(true); expect(metrics.alerts?.users?.total).to.be(4); expect( @@ -96,7 +96,7 @@ export default ({ getService }: FtrProviderContext): void => { { name: '7bgwxrbmcu', count: 1 }, { name: 'jf9e87gsut', count: 1 }, ]) - ); + ).to.be(true); }); }); diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/internal/suggest_user_profiles.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/internal/suggest_user_profiles.ts index 44245f9b10e12d..c10c7a6b63997c 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/internal/suggest_user_profiles.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/internal/suggest_user_profiles.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { suggestUserProfiles } from '../../../../common/lib/utils'; +import { suggestUserProfiles } from '../../../../common/lib/user_profiles'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/test/common/services/spaces.ts b/x-pack/test/common/services/spaces.ts index bae83a71d3c279..ad829e45fccecf 100644 --- a/x-pack/test/common/services/spaces.ts +++ b/x-pack/test/common/services/spaces.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { Space } from '@kbn/spaces-plugin/common'; import Axios from 'axios'; import { format as formatUrl } from 'url'; import util from 'util'; @@ -46,5 +47,19 @@ export function SpacesServiceProvider({ getService }: FtrProviderContext) { } log.debug(`deleted space id: ${spaceId}`); } + + public async getAll() { + log.debug('retrieving all spaces'); + const { data, status, statusText } = await axios.get<Space[]>('/api/spaces/space'); + + if (status !== 200) { + throw new Error( + `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}` + ); + } + log.debug(`retrieved ${data.length} spaces`); + + return data; + } })(); } diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts index a857757f2d8643..3064d412da1bd6 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts @@ -34,28 +34,5 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./find_rule_exception_references')); loadTestFile(require.resolve('./generating_signals')); loadTestFile(require.resolve('./get_prepackaged_rules_status')); - loadTestFile(require.resolve('./get_rule_execution_results')); - loadTestFile(require.resolve('./import_rules')); - loadTestFile(require.resolve('./import_export_rules')); - loadTestFile(require.resolve('./legacy_actions_migrations')); - loadTestFile(require.resolve('./read_rules')); - loadTestFile(require.resolve('./resolve_read_rules')); - loadTestFile(require.resolve('./update_rules')); - loadTestFile(require.resolve('./update_rules_bulk')); - loadTestFile(require.resolve('./patch_rules_bulk')); - loadTestFile(require.resolve('./perform_bulk_action')); - loadTestFile(require.resolve('./perform_bulk_action_dry_run')); - loadTestFile(require.resolve('./patch_rules')); - loadTestFile(require.resolve('./read_privileges')); - loadTestFile(require.resolve('./open_close_signals')); - loadTestFile(require.resolve('./get_signals_migration_status')); - loadTestFile(require.resolve('./create_signals_migrations')); - loadTestFile(require.resolve('./finalize_signals_migrations')); - loadTestFile(require.resolve('./delete_signals_migrations')); - loadTestFile(require.resolve('./timestamps')); - loadTestFile(require.resolve('./runtime')); - loadTestFile(require.resolve('./throttle')); - loadTestFile(require.resolve('./ignore_fields')); - loadTestFile(require.resolve('./migrations')); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/config.ts new file mode 100644 index 00000000000000..2430b8f2148d9a --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/create_signals_migrations.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_signals_migrations.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/create_signals_migrations.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/delete_signals_migrations.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_signals_migrations.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/delete_signals_migrations.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/finalize_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/finalize_signals_migrations.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/finalize_signals_migrations.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/finalize_signals_migrations.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_rule_execution_results.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/get_rule_execution_results.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_rule_execution_results.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/get_rule_execution_results.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_signals_migration_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/get_signals_migration_status.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_signals_migration_status.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/get_signals_migration_status.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/ignore_fields.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/ignore_fields.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/ignore_fields.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/ignore_fields.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/import_export_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/import_export_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/import_export_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/import_export_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/import_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/import_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/import_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/import_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/index.ts new file mode 100644 index 00000000000000..4449e9ca07800a --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/index.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled - Group 10', function () { + // !!NOTE: For new routes that do any updates on a rule, please ensure that you are including the legacy + // action migration code. We are monitoring legacy action telemetry to clean up once we see their + // existence being near 0. + + loadTestFile(require.resolve('./get_rule_execution_results')); + loadTestFile(require.resolve('./import_rules')); + loadTestFile(require.resolve('./import_export_rules')); + loadTestFile(require.resolve('./legacy_actions_migrations')); + loadTestFile(require.resolve('./read_rules')); + loadTestFile(require.resolve('./resolve_read_rules')); + loadTestFile(require.resolve('./update_rules')); + loadTestFile(require.resolve('./update_rules_bulk')); + loadTestFile(require.resolve('./patch_rules_bulk')); + loadTestFile(require.resolve('./perform_bulk_action')); + loadTestFile(require.resolve('./perform_bulk_action_dry_run')); + loadTestFile(require.resolve('./patch_rules')); + loadTestFile(require.resolve('./read_privileges')); + loadTestFile(require.resolve('./open_close_signals')); + loadTestFile(require.resolve('./get_signals_migration_status')); + loadTestFile(require.resolve('./create_signals_migrations')); + loadTestFile(require.resolve('./finalize_signals_migrations')); + loadTestFile(require.resolve('./delete_signals_migrations')); + loadTestFile(require.resolve('./timestamps')); + loadTestFile(require.resolve('./runtime')); + loadTestFile(require.resolve('./throttle')); + loadTestFile(require.resolve('./ignore_fields')); + loadTestFile(require.resolve('./migrations')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/legacy_actions_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/legacy_actions_migrations.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/legacy_actions_migrations.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/legacy_actions_migrations.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/migrations.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/migrations.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/migrations.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/open_close_signals.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/open_close_signals.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/open_close_signals.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/patch_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/patch_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/patch_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules_bulk.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/patch_rules_bulk.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules_bulk.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts similarity index 99% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts index 4d4bda5e6b4e0d..c7e49ac77e7be5 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts @@ -18,7 +18,7 @@ import { BulkAction, BulkActionEditType, } from '@kbn/security-solution-plugin/common/detection_engine/schemas/request/perform_bulk_action_schema'; -import { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response'; +import type { FullResponseSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/request'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { binaryToString, @@ -375,7 +375,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(rulesResponse.total).to.eql(2); - rulesResponse.data.forEach((rule: RulesSchema) => { + rulesResponse.data.forEach((rule: FullResponseSchema) => { expect(rule.actions).to.eql([ { action_type_id: '.slack', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action_dry_run.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action_dry_run.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action_dry_run.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action_dry_run.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/read_privileges.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/read_privileges.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/read_privileges.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/read_privileges.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/read_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/read_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/read_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/read_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/resolve_read_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/resolve_read_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/resolve_read_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/resolve_read_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/runtime.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/runtime.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/runtime.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/runtime.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/template_data/execution_events.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/template_data/execution_events.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/template_data/execution_events.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/template_data/execution_events.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/throttle.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/throttle.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/throttle.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/throttle.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/timestamps.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/timestamps.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/timestamps.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/timestamps.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts similarity index 99% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts index 49c1ba045d5e65..1b9dcb5da28fdd 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts @@ -82,6 +82,7 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); const outputRule = getSimpleMlRuleOutput(); + // @ts-expect-error type narrowing is lost due to Omit<> outputRule.machine_learning_job_id = ['legacy_job_id']; outputRule.version = 2; const bodyToCompare = removeServerGeneratedProperties(body); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts similarity index 97% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_rules_bulk.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts index 19447dec2b4a8b..dc7209b9f1c984 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts @@ -19,8 +19,6 @@ import { deleteSignalsIndex, getSimpleRuleOutput, removeServerGeneratedProperties, - getSimpleRuleOutputWithoutRuleId, - removeServerGeneratedPropertiesIncludingRuleId, getSimpleRuleUpdate, createRule, getSimpleRule, @@ -282,16 +280,16 @@ export default ({ getService }: FtrProviderContext) => { .send([updatedRule1, updatedRule2]) .expect(200); - const outputRule1 = getSimpleRuleOutputWithoutRuleId('rule-1'); + const outputRule1 = getSimpleRuleOutput('rule-1'); outputRule1.name = 'some other name'; outputRule1.version = 2; - const outputRule2 = getSimpleRuleOutputWithoutRuleId('rule-2'); + const outputRule2 = getSimpleRuleOutput('rule-2'); outputRule2.name = 'some other name'; outputRule2.version = 2; - const bodyToCompare1 = removeServerGeneratedPropertiesIncludingRuleId(body[0]); - const bodyToCompare2 = removeServerGeneratedPropertiesIncludingRuleId(body[1]); + const bodyToCompare1 = removeServerGeneratedProperties(body[0]); + const bodyToCompare2 = removeServerGeneratedProperties(body[1]); expect(bodyToCompare1).to.eql(outputRule1); expect(bodyToCompare2).to.eql(outputRule2); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/create_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/create_exceptions.ts index f44e72f5cd50a9..647c4dddb2bb13 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/create_exceptions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/create_exceptions.ts @@ -10,7 +10,7 @@ import expect from '@kbn/expect'; import type { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; -import { +import type { CreateRulesSchema, EqlCreateSchema, QueryCreateSchema, @@ -18,7 +18,6 @@ import { ThresholdCreateSchema, } from '@kbn/security-solution-plugin/common/detection_engine/schemas/request'; import { getCreateExceptionListItemMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_item_schema.mock'; -import { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response'; import { getCreateExceptionListMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_schema.mock'; import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; @@ -106,7 +105,7 @@ export default ({ getService }: FtrProviderContext) => { }; const rule = await createRule(supertest, log, ruleWithException); - const expected: Partial<RulesSchema> = { + const expected = { ...getSimpleRuleOutput(), exceptions_list: [ { @@ -147,7 +146,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccessOrStatus(supertest, log, rule.id); const bodyToCompare = removeServerGeneratedProperties(rule); - const expected: Partial<RulesSchema> = { + const expected = { ...getSimpleRuleOutput(), enabled: true, exceptions_list: [ diff --git a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule.ts b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule.ts index 1db5c784660fc4..4f5cfdcd3ba566 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule.ts @@ -5,13 +5,13 @@ * 2.0. */ -import type { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response/rules_schema'; +import type { CreateRulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/request'; /** * This will return a complex rule with all the outputs possible * @param ruleId The ruleId to set which is optional and defaults to rule-1 */ -export const getComplexRule = (ruleId = 'rule-1'): Partial<RulesSchema> => ({ +export const getComplexRule = (ruleId = 'rule-1'): CreateRulesSchema => ({ actions: [], author: [], name: 'Complex Rule Query', @@ -92,4 +92,6 @@ export const getComplexRule = (ruleId = 'rule-1'): Partial<RulesSchema> => ({ note: '# some investigation documentation', version: 1, query: 'user.name: root or user.name: admin', + throttle: 'no_actions', + exceptions_list: [], }); diff --git a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts index cc33c2ebff4472..1491829b339999 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts @@ -5,13 +5,15 @@ * 2.0. */ -import type { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response/rules_schema'; +import type { FullResponseSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/request'; +// TODO: Follow up https://github.com/elastic/kibana/pull/137628 and add an explicit type to this object +// without using Partial /** * This will return a complex rule with all the outputs possible * @param ruleId The ruleId to set which is optional and defaults to rule-1 */ -export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial<RulesSchema> => ({ +export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial<FullResponseSchema> => ({ actions: [], author: [], created_by: 'elastic', diff --git a/x-pack/test/detection_engine_api_integration/utils/get_rule.ts b/x-pack/test/detection_engine_api_integration/utils/get_rule.ts index da28e867bc9761..b1036e1f8b6821 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_rule.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_rule.ts @@ -7,7 +7,7 @@ import type { ToolingLog } from '@kbn/tooling-log'; import type SuperTest from 'supertest'; -import type { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response/rules_schema'; +import type { FullResponseSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/request'; import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; @@ -21,7 +21,7 @@ export const getRule = async ( supertest: SuperTest.SuperTest<SuperTest.Test>, log: ToolingLog, ruleId: string -): Promise<RulesSchema> => { +): Promise<FullResponseSchema> => { const response = await supertest .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=${ruleId}`) .set('kbn-xsrf', 'true'); diff --git a/x-pack/test/detection_engine_api_integration/utils/get_simple_ml_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/get_simple_ml_rule_output.ts index c845c0d343261d..56afa355b0482c 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_simple_ml_rule_output.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_simple_ml_rule_output.ts @@ -5,15 +5,13 @@ * 2.0. */ -import type { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response/rules_schema'; -import { getSimpleRuleOutput } from './get_simple_rule_output'; - -export const getSimpleMlRuleOutput = (ruleId = 'rule-1'): Partial<RulesSchema> => { - const rule = getSimpleRuleOutput(ruleId); - const { query, language, index, ...rest } = rule; +import type { MachineLearningResponseSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/request'; +import { getMockSharedResponseSchema } from './get_simple_rule_output'; +import { removeServerGeneratedProperties } from './remove_server_generated_properties'; +const getBaseMlRuleOutput = (ruleId = 'rule-1'): MachineLearningResponseSchema => { return { - ...rest, + ...getMockSharedResponseSchema(ruleId), name: 'Simple ML Rule', description: 'Simple Machine Learning Rule', anomaly_threshold: 44, @@ -21,3 +19,7 @@ export const getSimpleMlRuleOutput = (ruleId = 'rule-1'): Partial<RulesSchema> = type: 'machine_learning', }; }; + +export const getSimpleMlRuleOutput = (ruleId = 'rule-1') => { + return removeServerGeneratedProperties(getBaseMlRuleOutput(ruleId)); +}; diff --git a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts index 0d6cf9905d4a23..1fe2f2adecc799 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts @@ -5,13 +5,16 @@ * 2.0. */ -import type { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response/rules_schema'; +import type { + FullResponseSchema, + SharedResponseSchema, +} from '@kbn/security-solution-plugin/common/detection_engine/schemas/request'; +import { removeServerGeneratedProperties } from './remove_server_generated_properties'; -/** - * This is the typical output of a simple rule that Kibana will output with all the defaults - * except for the server generated properties. Useful for testing end to end tests. - */ -export const getSimpleRuleOutput = (ruleId = 'rule-1', enabled = false): Partial<RulesSchema> => ({ +export const getMockSharedResponseSchema = ( + ruleId = 'rule-1', + enabled = false +): SharedResponseSchema => ({ actions: [], author: [], created_by: 'elastic', @@ -20,10 +23,8 @@ export const getSimpleRuleOutput = (ruleId = 'rule-1', enabled = false): Partial false_positives: [], from: 'now-6m', immutable: false, - index: ['auditbeat-*'], interval: '5m', rule_id: ruleId, - language: 'kuery', output_index: '', max_signals: 100, related_integrations: [], @@ -31,17 +32,50 @@ export const getSimpleRuleOutput = (ruleId = 'rule-1', enabled = false): Partial risk_score: 1, risk_score_mapping: [], name: 'Simple Rule Query', - query: 'user.name: root or user.name: admin', references: [], setup: '', - severity: 'high', + severity: 'high' as const, severity_mapping: [], updated_by: 'elastic', tags: [], to: 'now', - type: 'query', threat: [], throttle: 'no_actions', exceptions_list: [], version: 1, + id: 'id', + updated_at: '2020-07-08T16:36:32.377Z', + created_at: '2020-07-08T16:36:32.377Z', + building_block_type: undefined, + note: undefined, + license: undefined, + outcome: undefined, + alias_target_id: undefined, + alias_purpose: undefined, + timeline_id: undefined, + timeline_title: undefined, + meta: undefined, + rule_name_override: undefined, + timestamp_override: undefined, + timestamp_override_fallback_disabled: undefined, + namespace: undefined, }); + +const getQueryRuleOutput = (ruleId = 'rule-1', enabled = false): FullResponseSchema => ({ + ...getMockSharedResponseSchema(ruleId, enabled), + index: ['auditbeat-*'], + language: 'kuery', + query: 'user.name: root or user.name: admin', + type: 'query', + data_view_id: undefined, + filters: undefined, + saved_id: undefined, +}); + +/** + * This is the typical output of a simple rule that Kibana will output with all the defaults + * except for the server generated properties. Useful for testing end to end tests. + */ +export const getSimpleRuleOutput = (ruleId = 'rule-1', enabled = false) => { + return removeServerGeneratedProperties(getQueryRuleOutput(ruleId, enabled)); +}; diff --git a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_with_web_hook_action.ts b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_with_web_hook_action.ts index 45dd0bfd5d477a..c96537bfd08134 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_with_web_hook_action.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_with_web_hook_action.ts @@ -5,10 +5,12 @@ * 2.0. */ -import type { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response/rules_schema'; import { getSimpleRuleOutput } from './get_simple_rule_output'; +import { RuleWithoutServerGeneratedProperties } from './remove_server_generated_properties'; -export const getSimpleRuleOutputWithWebHookAction = (actionId: string): Partial<RulesSchema> => ({ +export const getSimpleRuleOutputWithWebHookAction = ( + actionId: string +): RuleWithoutServerGeneratedProperties => ({ ...getSimpleRuleOutput(), throttle: 'rule', actions: [ diff --git a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_without_rule_id.ts b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_without_rule_id.ts index dbf94965278d6a..56b5ab66773bba 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_without_rule_id.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_without_rule_id.ts @@ -5,14 +5,16 @@ * 2.0. */ -import type { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response/rules_schema'; import { getSimpleRuleOutput } from './get_simple_rule_output'; +import { RuleWithoutServerGeneratedProperties } from './remove_server_generated_properties'; /** * This is the typical output of a simple rule that Kibana will output with all the defaults except * for all the server generated properties such as created_by. Useful for testing end to end tests. */ -export const getSimpleRuleOutputWithoutRuleId = (ruleId = 'rule-1'): Partial<RulesSchema> => { +export const getSimpleRuleOutputWithoutRuleId = ( + ruleId = 'rule-1' +): Omit<RuleWithoutServerGeneratedProperties, 'rule_id'> => { const rule = getSimpleRuleOutput(ruleId); const { rule_id: rId, ...ruleWithoutRuleId } = rule; return ruleWithoutRuleId; diff --git a/x-pack/test/detection_engine_api_integration/utils/remove_server_generated_properties.ts b/x-pack/test/detection_engine_api_integration/utils/remove_server_generated_properties.ts index 5f863c0e62b9b3..8d8a34bba8b796 100644 --- a/x-pack/test/detection_engine_api_integration/utils/remove_server_generated_properties.ts +++ b/x-pack/test/detection_engine_api_integration/utils/remove_server_generated_properties.ts @@ -6,6 +6,15 @@ */ import type { FullResponseSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/request'; +import { omit, pickBy } from 'lodash'; + +const serverGeneratedProperties = ['id', 'created_at', 'updated_at', 'execution_summary'] as const; + +type ServerGeneratedProperties = typeof serverGeneratedProperties[number]; +export type RuleWithoutServerGeneratedProperties = Omit< + FullResponseSchema, + ServerGeneratedProperties +>; /** * This will remove server generated properties such as date times, etc... @@ -13,14 +22,12 @@ import type { FullResponseSchema } from '@kbn/security-solution-plugin/common/de */ export const removeServerGeneratedProperties = ( rule: FullResponseSchema -): Partial<FullResponseSchema> => { - const { - /* eslint-disable @typescript-eslint/naming-convention */ - id, - created_at, - updated_at, - execution_summary, - ...removedProperties - } = rule; - return removedProperties; +): RuleWithoutServerGeneratedProperties => { + const removedProperties = omit(rule, serverGeneratedProperties); + + // We're only removing undefined values, so this cast correctly narrows the type + return pickBy( + removedProperties, + (value) => value !== undefined + ) as RuleWithoutServerGeneratedProperties; }; diff --git a/x-pack/test/detection_engine_api_integration/utils/resolve_simple_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/resolve_simple_rule_output.ts index 468cbdfa23aa58..4f8b24e623ac32 100644 --- a/x-pack/test/detection_engine_api_integration/utils/resolve_simple_rule_output.ts +++ b/x-pack/test/detection_engine_api_integration/utils/resolve_simple_rule_output.ts @@ -5,11 +5,9 @@ * 2.0. */ -import type { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response/rules_schema'; - import { getSimpleRuleOutput } from './get_simple_rule_output'; -export const resolveSimpleRuleOutput = ( - ruleId = 'rule-1', - enabled = false -): Partial<RulesSchema> => ({ outcome: 'exactMatch', ...getSimpleRuleOutput(ruleId, enabled) }); +export const resolveSimpleRuleOutput = (ruleId = 'rule-1', enabled = false) => ({ + ...getSimpleRuleOutput(ruleId, enabled), + outcome: 'exactMatch', +}); diff --git a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts index 7c97ff5b79bf9a..746a2fcda1ba17 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts @@ -199,7 +199,7 @@ export default function (providerContext: FtrProviderContext) { }); it('should bulk reassign multiple agents by kuery in batches', async () => { - const { body: unenrolledBody } = await supertest + const { body } = await supertest .post(`/api/fleet/agents/bulk_reassign`) .set('kbn-xsrf', 'xxx') .send({ @@ -209,17 +209,37 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); - expect(unenrolledBody).to.eql({ - agent1: { success: true }, - agent2: { success: true }, - agent3: { success: true }, - agent4: { success: true }, - }); + const actionId = body.actionId; - const { body } = await supertest.get(`/api/fleet/agents`).set('kbn-xsrf', 'xxx'); - expect(body.total).to.eql(4); - body.items.forEach((agent: any) => { - expect(agent.policy_id).to.eql('policy2'); + const verifyActionResult = async () => { + const { body: result } = await supertest.get(`/api/fleet/agents`).set('kbn-xsrf', 'xxx'); + expect(result.total).to.eql(4); + result.items.forEach((agent: any) => { + expect(agent.policy_id).to.eql('policy2'); + }); + }; + + await new Promise((resolve, reject) => { + let attempts = 0; + const intervalId = setInterval(async () => { + if (attempts > 2) { + clearInterval(intervalId); + reject('action timed out'); + } + ++attempts; + const { + body: { items: actionStatuses }, + } = await supertest.get(`/api/fleet/agents/action_status`).set('kbn-xsrf', 'xxx'); + + const action = actionStatuses.find((a: any) => a.actionId === actionId); + if (action && action.nbAgentsActioned === action.nbAgentsActionCreated) { + clearInterval(intervalId); + await verifyActionResult(); + resolve({}); + } + }, 1000); + }).catch((e) => { + throw e; }); }); diff --git a/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts b/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts index 93d0a58b848df1..3956dcafad7052 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts @@ -198,26 +198,40 @@ export default function (providerContext: FtrProviderContext) { expect(body.total).to.eql(0); }); - it('/agents/bulk_unenroll should allow to unenroll multiple agents by kuery in batches', async () => { - const { body: unenrolledBody } = await supertest + it('/agents/bulk_unenroll should allow to unenroll multiple agents by kuery in batches async', async () => { + const { body } = await supertest .post(`/api/fleet/agents/bulk_unenroll`) .set('kbn-xsrf', 'xxx') .send({ agents: 'active: true', - revoke: true, + revoke: false, batchSize: 2, }) .expect(200); - expect(unenrolledBody).to.eql({ - agent1: { success: true }, - agent2: { success: true }, - agent3: { success: true }, - agent4: { success: true }, + const actionId = body.actionId; + + await new Promise((resolve, reject) => { + let attempts = 0; + const intervalId = setInterval(async () => { + if (attempts > 2) { + clearInterval(intervalId); + reject('action timed out'); + } + ++attempts; + const { + body: { items: actionStatuses }, + } = await supertest.get(`/api/fleet/agents/action_status`).set('kbn-xsrf', 'xxx'); + + const action = actionStatuses.find((a: any) => a.actionId === actionId); + if (action && action.nbAgentsActioned === action.nbAgentsActionCreated) { + clearInterval(intervalId); + resolve({}); + } + }, 1000); + }).catch((e) => { + throw e; }); - - const { body } = await supertest.get(`/api/fleet/agents`); - expect(body.total).to.eql(0); }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts b/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts index 2de75be2e50b00..afe9f8d677d354 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts @@ -88,7 +88,7 @@ export default function (providerContext: FtrProviderContext) { }); it('should bulk update tags of multiple agents by kuery in batches', async () => { - const { body: updatedBody } = await supertest + await supertest .post(`/api/fleet/agents/bulk_update_agent_tags`) .set('kbn-xsrf', 'xxx') .send({ @@ -99,18 +99,18 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); - expect(updatedBody).to.eql({ - agent1: { success: true }, - agent2: { success: true }, - agent3: { success: true }, - agent4: { success: true }, - }); - - const { body } = await supertest.get(`/api/fleet/agents`).set('kbn-xsrf', 'xxx'); - expect(body.total).to.eql(4); - body.items.forEach((agent: any) => { - expect(agent.tags.includes('newTag')).to.be(true); - expect(agent.tags.includes('existingTag')).to.be(false); + await new Promise((resolve, reject) => { + setTimeout(async () => { + const { body } = await supertest.get(`/api/fleet/agents`).set('kbn-xsrf', 'xxx'); + expect(body.total).to.eql(4); + body.items.forEach((agent: any) => { + expect(agent.tags.includes('newTag')).to.be(true); + expect(agent.tags.includes('existingTag')).to.be(false); + }); + resolve({}); + }, 2000); + }).catch((e) => { + throw e; }); }); diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts index 9b0a6586e1c25e..b842f89c8ac644 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts @@ -540,7 +540,7 @@ export default function (providerContext: FtrProviderContext) { expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); }); - it('should bulk upgrade multiple agents by kuery in batches', async () => { + it('should bulk upgrade multiple agents by kuery in batches async', async () => { await es.update({ id: 'agent1', refresh: 'wait_for', @@ -557,17 +557,12 @@ export default function (providerContext: FtrProviderContext) { index: AGENTS_INDEX, body: { doc: { - local_metadata: { - elastic: { - agent: { upgradeable: false, version: '0.0.0' }, - }, - }, - upgrade_started_at: undefined, + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, }, }, }); - const { body: unenrolledBody } = await supertest + const { body } = await supertest .post(`/api/fleet/agents/bulk_upgrade`) .set('kbn-xsrf', 'xxx') .send({ @@ -577,12 +572,38 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); - expect(unenrolledBody).to.eql({ - agent4: { success: false, error: 'agent4 is not upgradeable' }, - agent3: { success: false, error: 'agent3 is not upgradeable' }, - agent2: { success: false, error: 'agent2 is not upgradeable' }, - agent1: { success: true }, - agentWithFS: { success: false, error: 'agentWithFS is not upgradeable' }, + const actionId = body.actionId; + + const verifyActionResult = async () => { + const [agent1data, agent2data] = await Promise.all([ + supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), + ]); + expect(typeof agent1data.body.item.upgrade_started_at).to.be('string'); + expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); + }; + + await new Promise((resolve, reject) => { + let attempts = 0; + const intervalId = setInterval(async () => { + if (attempts > 2) { + clearInterval(intervalId); + reject('action timed out'); + } + ++attempts; + const { + body: { items: actionStatuses }, + } = await supertest.get(`/api/fleet/agents/action_status`).set('kbn-xsrf', 'xxx'); + const action = actionStatuses.find((a: any) => a.actionId === actionId); + // 2 upgradeable + if (action && action.nbAgentsActionCreated === 2) { + clearInterval(intervalId); + await verifyActionResult(); + resolve({}); + } + }, 1000); + }).catch((e) => { + throw e; }); }); diff --git a/x-pack/test/functional/apps/home/feature_controls/home_security.ts b/x-pack/test/functional/apps/home/feature_controls/home_security.ts index a48bc7651a1ed1..831f0475c2c11f 100644 --- a/x-pack/test/functional/apps/home/feature_controls/home_security.ts +++ b/x-pack/test/functional/apps/home/feature_controls/home_security.ts @@ -35,7 +35,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); }); - // https://github.com/elastic/kibana/issues/132628 describe('global all privileges', () => { before(async () => { await security.role.create('global_all_role', { diff --git a/x-pack/test/functional/apps/ml/short_tests/embeddables/anomaly_charts_dashboard_embeddables.ts b/x-pack/test/functional/apps/ml/anomaly_detection_integrations/anomaly_charts_dashboard_embeddables.ts similarity index 91% rename from x-pack/test/functional/apps/ml/short_tests/embeddables/anomaly_charts_dashboard_embeddables.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_integrations/anomaly_charts_dashboard_embeddables.ts index ef674c1744a511..f2273b168489b6 100644 --- a/x-pack/test/functional/apps/ml/short_tests/embeddables/anomaly_charts_dashboard_embeddables.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_integrations/anomaly_charts_dashboard_embeddables.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; import { JOB_CONFIG, DATAFEED_CONFIG, ML_EMBEDDABLE_TYPES } from './constants'; const testDataList = [ @@ -32,6 +32,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); const PageObjects = getPageObjects(['common', 'timePicker', 'dashboard']); + const from = 'Feb 7, 2016 @ 00:00:00.000'; + const to = 'Feb 11, 2016 @ 00:00:00.000'; describe('anomaly charts in dashboard', function () { this.tags(['ml']); @@ -41,11 +43,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); await ml.testResources.setKibanaTimeZoneToUTC(); await ml.securityUI.loginAsMlPowerUser(); + await PageObjects.common.setTime({ from, to }); }); after(async () => { await ml.api.cleanMlIndices(); await ml.testResources.deleteIndexPatternByTitle('ft_farequote'); + await PageObjects.common.unsetTime(); }); for (const testData of testDataList) { @@ -80,13 +84,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('create new anomaly charts panel', async () => { await ml.dashboardEmbeddables.clickInitializerConfirmButtonEnabled(); await ml.dashboardEmbeddables.assertDashboardPanelExists(testData.panelTitle); - - await ml.dashboardEmbeddables.assertNoMatchingAnomaliesMessageExists(); - - await PageObjects.timePicker.setAbsoluteRange( - 'Feb 7, 2016 @ 00:00:00.000', - 'Feb 11, 2016 @ 00:00:00.000' - ); await PageObjects.timePicker.pauseAutoRefresh(); await ml.dashboardEmbeddables.assertAnomalyChartsSeverityThresholdControlExists(); await ml.dashboardEmbeddables.assertAnomalyChartsExists(); diff --git a/x-pack/test/functional/apps/ml/short_tests/embeddables/anomaly_embeddables_migration.ts b/x-pack/test/functional/apps/ml/anomaly_detection_integrations/anomaly_embeddables_migration.ts similarity index 98% rename from x-pack/test/functional/apps/ml/short_tests/embeddables/anomaly_embeddables_migration.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_integrations/anomaly_embeddables_migration.ts index 8f3c30a15e5433..7058286f3d5b3a 100644 --- a/x-pack/test/functional/apps/ml/short_tests/embeddables/anomaly_embeddables_migration.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_integrations/anomaly_embeddables_migration.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; import { JOB_CONFIG, DATAFEED_CONFIG, ML_EMBEDDABLE_TYPES } from './constants'; const testDataList = [ diff --git a/x-pack/test/visual_regression/config.ts b/x-pack/test/functional/apps/ml/anomaly_detection_integrations/config.ts similarity index 61% rename from x-pack/test/visual_regression/config.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_integrations/config.ts index c7f0d8203833e2..363a72c4c0310c 100644 --- a/x-pack/test/visual_regression/config.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_integrations/config.ts @@ -7,25 +7,14 @@ import { FtrConfigProviderContext } from '@kbn/test'; -import { services } from './services'; - export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); return { ...functionalConfig.getAll(), - - testFiles: [ - require.resolve('./tests/canvas'), - require.resolve('./tests/login_page'), - require.resolve('./tests/maps'), - require.resolve('./tests/infra'), - ], - - services, - + testFiles: [require.resolve('.')], junit: { - reportName: 'X-Pack Visual Regression Tests', + reportName: 'Chrome X-Pack UI Functional Tests - ML anomaly_detection_integrations', }, }; } diff --git a/x-pack/test/functional/apps/ml/short_tests/embeddables/constants.ts b/x-pack/test/functional/apps/ml/anomaly_detection_integrations/constants.ts similarity index 100% rename from x-pack/test/functional/apps/ml/short_tests/embeddables/constants.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_integrations/constants.ts diff --git a/x-pack/test/functional/apps/ml/anomaly_detection_integrations/index.ts b/x-pack/test/functional/apps/ml/anomaly_detection_integrations/index.ts new file mode 100644 index 00000000000000..1859ab5a41d7fa --- /dev/null +++ b/x-pack/test/functional/apps/ml/anomaly_detection_integrations/index.ts @@ -0,0 +1,39 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('machine learning - anomaly detection', function () { + this.tags(['skipFirefox']); + + before(async () => { + await ml.securityCommon.createMlRoles(); + await ml.securityCommon.createMlUsers(); + }); + + after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await ml.securityUI.logout(); + + await ml.securityCommon.cleanMlUsers(); + await ml.securityCommon.cleanMlRoles(); + + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); + + await ml.testResources.resetKibanaTimeZone(); + }); + + loadTestFile(require.resolve('./anomaly_charts_dashboard_embeddables')); + loadTestFile(require.resolve('./anomaly_embeddables_migration')); + loadTestFile(require.resolve('./lens_to_ml')); + loadTestFile(require.resolve('./lens_to_ml_with_wizard')); + }); +} diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/lens_to_ml.ts b/x-pack/test/functional/apps/ml/anomaly_detection_integrations/lens_to_ml.ts similarity index 100% rename from x-pack/test/functional/apps/ml/anomaly_detection/lens_to_ml.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_integrations/lens_to_ml.ts diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/lens_to_ml_with_wizard.ts b/x-pack/test/functional/apps/ml/anomaly_detection_integrations/lens_to_ml_with_wizard.ts similarity index 100% rename from x-pack/test/functional/apps/ml/anomaly_detection/lens_to_ml_with_wizard.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_integrations/lens_to_ml_with_wizard.ts diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/advanced_job.ts similarity index 100% rename from x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_jobs/advanced_job.ts diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/categorization_job.ts similarity index 100% rename from x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_jobs/categorization_job.ts diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/config.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/config.ts similarity index 97% rename from x-pack/test/functional/apps/ml/anomaly_detection/config.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_jobs/config.ts index 9078782e36f0b5..c2bd1f7dbedfa4 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/config.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/config.ts @@ -14,7 +14,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...functionalConfig.getAll(), testFiles: [require.resolve('.')], junit: { - reportName: 'Chrome X-Pack UI Functional Tests - ML anomaly_detection', + reportName: 'Chrome X-Pack UI Functional Tests - ML anomaly_detection_jobs', }, }; } diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/custom_urls.ts similarity index 100% rename from x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_jobs/custom_urls.ts diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/date_nanos_job.ts similarity index 100% rename from x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_jobs/date_nanos_job.ts diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/index.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/index.ts similarity index 82% rename from x-pack/test/functional/apps/ml/anomaly_detection/index.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_jobs/index.ts index 35bfd4471233a9..e2f6901b75c315 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/index.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/index.ts @@ -40,15 +40,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./population_job')); loadTestFile(require.resolve('./saved_search_job')); loadTestFile(require.resolve('./advanced_job')); - loadTestFile(require.resolve('./single_metric_viewer')); - loadTestFile(require.resolve('./anomaly_explorer')); loadTestFile(require.resolve('./categorization_job')); loadTestFile(require.resolve('./date_nanos_job')); - loadTestFile(require.resolve('./annotations')); - loadTestFile(require.resolve('./aggregated_scripted_job')); loadTestFile(require.resolve('./custom_urls')); - loadTestFile(require.resolve('./forecasts')); - loadTestFile(require.resolve('./lens_to_ml')); - loadTestFile(require.resolve('./lens_to_ml_with_wizard')); }); } diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/multi_metric_job.ts similarity index 100% rename from x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_jobs/multi_metric_job.ts diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/population_job.ts similarity index 100% rename from x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_jobs/population_job.ts diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/saved_search_job.ts similarity index 100% rename from x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_jobs/saved_search_job.ts diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/single_metric_job.ts similarity index 100% rename from x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_jobs/single_metric_job.ts diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/single_metric_job_without_datafeed_start.ts similarity index 100% rename from x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_jobs/single_metric_job_without_datafeed_start.ts diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection_result_views/aggregated_scripted_job.ts similarity index 100% rename from x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_result_views/aggregated_scripted_job.ts diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts b/x-pack/test/functional/apps/ml/anomaly_detection_result_views/annotations.ts similarity index 100% rename from x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_result_views/annotations.ts diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts b/x-pack/test/functional/apps/ml/anomaly_detection_result_views/anomaly_explorer.ts similarity index 100% rename from x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_result_views/anomaly_explorer.ts diff --git a/x-pack/test/functional/apps/ml/anomaly_detection_result_views/config.ts b/x-pack/test/functional/apps/ml/anomaly_detection_result_views/config.ts new file mode 100644 index 00000000000000..c164a18f6df431 --- /dev/null +++ b/x-pack/test/functional/apps/ml/anomaly_detection_result_views/config.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + junit: { + reportName: 'Chrome X-Pack UI Functional Tests - ML anomaly_detection_result_views', + }, + }; +} diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts b/x-pack/test/functional/apps/ml/anomaly_detection_result_views/forecasts.ts similarity index 100% rename from x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_result_views/forecasts.ts diff --git a/x-pack/test/functional/apps/ml/anomaly_detection_result_views/index.ts b/x-pack/test/functional/apps/ml/anomaly_detection_result_views/index.ts new file mode 100644 index 00000000000000..13f7ac9e97b09c --- /dev/null +++ b/x-pack/test/functional/apps/ml/anomaly_detection_result_views/index.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('machine learning - anomaly detection', function () { + this.tags(['skipFirefox']); + + before(async () => { + await ml.securityCommon.createMlRoles(); + await ml.securityCommon.createMlUsers(); + }); + + after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await ml.securityUI.logout(); + + await ml.securityCommon.cleanMlUsers(); + await ml.securityCommon.cleanMlRoles(); + + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); + + await ml.testResources.resetKibanaTimeZone(); + }); + + loadTestFile(require.resolve('./aggregated_scripted_job')); + loadTestFile(require.resolve('./annotations')); + loadTestFile(require.resolve('./anomaly_explorer')); + loadTestFile(require.resolve('./forecasts')); + loadTestFile(require.resolve('./single_metric_viewer')); + }); +} diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts b/x-pack/test/functional/apps/ml/anomaly_detection_result_views/single_metric_viewer.ts similarity index 100% rename from x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts rename to x-pack/test/functional/apps/ml/anomaly_detection_result_views/single_metric_viewer.ts diff --git a/x-pack/test/functional/apps/ml/short_tests/embeddables/index.ts b/x-pack/test/functional/apps/ml/short_tests/embeddables/index.ts deleted file mode 100644 index d786491e55a4ef..00000000000000 --- a/x-pack/test/functional/apps/ml/short_tests/embeddables/index.ts +++ /dev/null @@ -1,16 +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 { FtrProviderContext } from '../../../../ftr_provider_context'; - -export default function ({ loadTestFile }: FtrProviderContext) { - describe('embeddables', function () { - this.tags(['skipFirefox']); - loadTestFile(require.resolve('./anomaly_charts_dashboard_embeddables')); - loadTestFile(require.resolve('./anomaly_embeddables_migration')); - }); -} diff --git a/x-pack/test/functional/apps/ml/short_tests/index.ts b/x-pack/test/functional/apps/ml/short_tests/index.ts index 3c4cbbc0677bea..f96d2b91ee0ef7 100644 --- a/x-pack/test/functional/apps/ml/short_tests/index.ts +++ b/x-pack/test/functional/apps/ml/short_tests/index.ts @@ -33,6 +33,5 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./model_management')); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./settings')); - loadTestFile(require.resolve('./embeddables')); }); } diff --git a/x-pack/test/functional/apps/rollup_job/tsvb.js b/x-pack/test/functional/apps/rollup_job/tsvb.js index 4ef918aab09e4d..957b33618d9cfb 100644 --- a/x-pack/test/functional/apps/rollup_job/tsvb.js +++ b/x-pack/test/functional/apps/rollup_job/tsvb.js @@ -21,6 +21,8 @@ export default function ({ getService, getPageObjects }) { 'visualBuilder', 'timePicker', ]); + const fromTime = 'Oct 15, 2019 @ 00:00:01.000'; + const toTime = 'Oct 15, 2019 @ 19:31:44.000'; describe('tsvb integration', function () { //Since rollups can only be created once with the same name (even if you delete it), @@ -40,10 +42,11 @@ export default function ({ getService, getPageObjects }) { await kibanaServer.importExport.load( 'x-pack/test/functional/fixtures/kbn_archiver/rollup/rollup.json' ); - await kibanaServer.uiSettings.replace({ + await kibanaServer.uiSettings.update({ defaultIndex: 'rollup', + 'metrics:allowStringIndices': true, + 'timepicker:timeDefaults': `{ "from": "${fromTime}", "to": "${toTime}"}`, }); - await kibanaServer.uiSettings.update({ 'metrics:allowStringIndices': true }); }); it('create rollup tsvb', async () => { @@ -85,10 +88,6 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualBuilder.checkVisualBuilderIsPresent(); await PageObjects.visualBuilder.clickMetric(); await PageObjects.visualBuilder.checkMetricTabIsPresent(); - await PageObjects.timePicker.setAbsoluteRange( - 'Oct 15, 2019 @ 00:00:01.000', - 'Oct 15, 2019 @ 19:31:44.000' - ); await PageObjects.visualBuilder.clickPanelOptions('metric'); await PageObjects.visualBuilder.setIndexPatternValue(rollupTargetIndexName, false); await PageObjects.visualBuilder.selectIndexPatternTimeField('@timestamp'); @@ -112,6 +111,7 @@ export default function ({ getService, getPageObjects }) { 'x-pack/test/functional/fixtures/kbn_archiver/rollup/rollup.json' ); await kibanaServer.uiSettings.update({ 'metrics:allowStringIndices': false }); + await kibanaServer.uiSettings.replace({}); await security.testUser.restoreDefaults(); }); }); diff --git a/x-pack/test/functional/apps/spaces/copy_saved_objects.ts b/x-pack/test/functional/apps/spaces/copy_saved_objects.ts index 2d2fdf61a94b68..6260d2941956bf 100644 --- a/x-pack/test/functional/apps/spaces/copy_saved_objects.ts +++ b/x-pack/test/functional/apps/spaces/copy_saved_objects.ts @@ -8,18 +8,28 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -export default function spaceSelectorFunctonalTests({ +export default function spaceSelectorFunctionalTests({ getService, getPageObjects, }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); + const kbnServer = getService('kibanaServer'); const spaces = getService('spaces'); const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['security', 'settings', 'copySavedObjectsToSpace']); + const log = getService('log'); describe('Copy Saved Objects to Space', function () { before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/spaces/copy_saved_objects'); + log.debug('Loading test data for the following spaces: default, sales'); + await Promise.all([ + kbnServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/spaces/copy_saved_objects_default_space.json' + ), + kbnServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/spaces/copy_saved_objects_sales_space.json', + { space: 'sales' } + ), + ]); await spaces.create({ id: 'marketing', @@ -43,9 +53,15 @@ export default function spaceSelectorFunctonalTests({ }); after(async () => { + log.debug('Removing data from the following spaces: default, sales'); + await Promise.all( + ['default', 'sales'].map((spaceId) => + kbnServer.savedObjects.cleanStandardList({ space: spaceId }) + ) + ); + await spaces.delete('sales'); await spaces.delete('marketing'); - await esArchiver.unload('x-pack/test/functional/es_archives/spaces/copy_saved_objects'); }); it('allows a dashboard to be copied to the marketing space, with all references', async () => { diff --git a/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json b/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json deleted file mode 100644 index 552142d3b190ae..00000000000000 --- a/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json +++ /dev/null @@ -1,171 +0,0 @@ -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "space:default", - "source": { - "space": { - "name": "Default", - "description": "This is the default space!", - "disabledFeatures": [], - "_reserved": true - }, - "type": "space", - "migrationVersion": { - "space": "6.6.0" - } - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "index-pattern:logstash-*", - "source": { - "index-pattern": { - "title": "logstash-*", - "timeFieldName": "@timestamp", - "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" - }, - "type": "index-pattern", - "migrationVersion": { - "index-pattern": "6.5.0" - }, - "updated_at": "2018-12-21T00:43:07.096Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "sales:index-pattern:logstash-*", - "source": { - "namespace": "sales", - "index-pattern": { - "title": "logstash-*", - "timeFieldName": "@timestamp", - "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" - }, - "type": "index-pattern", - "migrationVersion": { - "index-pattern": "6.5.0" - }, - "updated_at": "2018-12-21T00:43:07.096Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "visualization:75c3e060-1e7c-11e9-8488-65449e65d0ed", - "source": { - "visualization": { - "title": "A Pie", - "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", - "uiStateJSON": "{}", - "description": "", - "version": 1, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" - } - }, - "type": "visualization", - "updated_at": "2019-01-22T19:32:31.206Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "dashboard:my-dashboard", - "source": { - "dashboard": { - "title": "A Dashboard", - "hits": 0, - "description": "", - "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.0.0\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"75c3e060-1e7c-11e9-8488-65449e65d0ed\",\"embeddableConfig\":{}}]", - "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", - "version": 1, - "timeRestore": false, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" - } - }, - "type": "dashboard", - "updated_at": "2019-01-22T19:32:47.232Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "dashboard:dashboard-foo", - "source": { - "references": [{ - "id":"dashboard-bar", - "name":"dashboard-circular-ref", - "type":"dashboard" - }], - "dashboard": { - "title": "Dashboard Foo", - "hits": 0, - "description": "", - "panelsJSON": "[]", - "optionsJSON": "{}", - "version": 1, - "timeRestore": false, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" - } - }, - "type": "dashboard", - "updated_at": "2019-01-22T19:32:47.232Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "dashboard:dashboard-bar", - "source": { - "references": [{ - "id":"dashboard-foo", - "name":"dashboard-circular-ref", - "type":"dashboard" - }], - "dashboard": { - "title": "Dashboard Bar", - "hits": 0, - "description": "", - "panelsJSON": "[]", - "optionsJSON": "{}", - "version": 1, - "timeRestore": false, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" - } - }, - "type": "dashboard", - "updated_at": "2019-01-22T19:32:47.232Z" - } - } -} diff --git a/x-pack/test/functional/es_archives/spaces/copy_saved_objects/mappings.json b/x-pack/test/functional/es_archives/spaces/copy_saved_objects/mappings.json deleted file mode 100644 index 092f8a326d9dfd..00000000000000 --- a/x-pack/test/functional/es_archives/spaces/copy_saved_objects/mappings.json +++ /dev/null @@ -1,293 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": {} - }, - "index": ".kibana_1", - "mappings": { - "dynamic": "strict", - "properties": { - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - }, - "dateFormat:tz": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "defaultIndex": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "notifications:lifetime:banner": { - "type": "long" - }, - "notifications:lifetime:error": { - "type": "long" - }, - "notifications:lifetime:info": { - "type": "long" - }, - "notifications:lifetime:warning": { - "type": "long" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - } - } - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - }, - "namespace": { - "type": "keyword" - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "initials": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "references": { - "type": "nested", - "properties": { - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "id": { - "type": "keyword" - } - } - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "number_of_replicas": "0", - "number_of_shards": "1" - } - } - } -} \ No newline at end of file diff --git a/x-pack/test/functional/fixtures/kbn_archiver/spaces/copy_saved_objects_default_space.json b/x-pack/test/functional/fixtures/kbn_archiver/spaces/copy_saved_objects_default_space.json new file mode 100644 index 00000000000000..a86e88d114e7fe --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/spaces/copy_saved_objects_default_space.json @@ -0,0 +1,134 @@ +{ + "attributes": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "8.5.0", + "id": "logstash-*", + "migrationVersion": { + "index-pattern": "8.0.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z", + "version": "WzksMl0=" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "A Pie", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"distinctColors\":true,\"legendDisplay\":\"show\",\"legendSize\":\"auto\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}" + }, + "coreMigrationVersion": "8.5.0", + "id": "75c3e060-1e7c-11e9-8488-65449e65d0ed", + "migrationVersion": { + "visualization": "8.5.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z", + "version": "WzEyLDJd" +} + +{ + "attributes": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"}]", + "timeRestore": false, + "title": "A Dashboard", + "version": 1 + }, + "coreMigrationVersion": "8.5.0", + "id": "my-dashboard", + "migrationVersion": { + "dashboard": "8.5.0" + }, + "references": [ + { + "id": "75c3e060-1e7c-11e9-8488-65449e65d0ed", + "name": "1:panel_1", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2019-01-22T19:32:47.232Z", + "version": "WzEzLDJd" +} + +{ + "attributes": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + }, + "optionsJSON": "{}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "Dashboard Bar", + "version": 1 + }, + "coreMigrationVersion": "8.5.0", + "id": "dashboard-bar", + "migrationVersion": { + "dashboard": "8.5.0" + }, + "references": [ + { + "id": "dashboard-foo", + "name": "dashboard-circular-ref", + "type": "dashboard" + } + ], + "type": "dashboard", + "updated_at": "2019-01-22T19:32:47.232Z", + "version": "WzE1LDJd" +} + +{ + "attributes": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + }, + "optionsJSON": "{}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "Dashboard Foo", + "version": 1 + }, + "coreMigrationVersion": "8.5.0", + "id": "dashboard-foo", + "migrationVersion": { + "dashboard": "8.5.0" + }, + "references": [ + { + "id": "dashboard-bar", + "name": "dashboard-circular-ref", + "type": "dashboard" + } + ], + "type": "dashboard", + "updated_at": "2019-01-22T19:32:47.232Z", + "version": "WzE0LDJd" +} diff --git a/x-pack/test/functional/fixtures/kbn_archiver/spaces/copy_saved_objects_sales_space.json b/x-pack/test/functional/fixtures/kbn_archiver/spaces/copy_saved_objects_sales_space.json new file mode 100644 index 00000000000000..b5505aab8c3b0d --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/spaces/copy_saved_objects_sales_space.json @@ -0,0 +1,17 @@ +{ + "attributes": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "8.5.0", + "id": "fd534677-dd27-526a-bb03-37e3fbc9db5d", + "migrationVersion": { + "index-pattern": "8.0.0" + }, + "originId": "logstash-*", + "references": [], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z", + "version": "WzEwLDJd" +} diff --git a/x-pack/test/functional/services/cases/api.ts b/x-pack/test/functional/services/cases/api.ts index ad4678adcafc35..983ee667a2ef80 100644 --- a/x-pack/test/functional/services/cases/api.ts +++ b/x-pack/test/functional/services/cases/api.ts @@ -13,12 +13,19 @@ import { createComment, updateCase, } from '../../../cases_api_integration/common/lib/utils'; +import { + loginUsers, + suggestUserProfiles, +} from '../../../cases_api_integration/common/lib/user_profiles'; +import { User } from '../../../cases_api_integration/common/lib/authentication/types'; + import { FtrProviderContext } from '../../ftr_provider_context'; import { generateRandomCaseWithoutConnector } from './helpers'; export function CasesAPIServiceProvider({ getService }: FtrProviderContext) { const kbnSupertest = getService('supertest'); const es = getService('es'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); return { async createCase(overwrites: Partial<CasePostRequest> = {}): Promise<CaseResponse> { @@ -76,5 +83,16 @@ export function CasesAPIServiceProvider({ getService }: FtrProviderContext) { }, }); }, + + async activateUserProfiles(users: User[]) { + await loginUsers({ + supertest: supertestWithoutAuth, + users, + }); + }, + + async suggestUserProfiles(options: Parameters<typeof suggestUserProfiles>[0]['req']) { + return suggestUserProfiles({ supertest: kbnSupertest, req: options }); + }, }; } diff --git a/x-pack/test/functional/services/cases/common.ts b/x-pack/test/functional/services/cases/common.ts index 5b854979adfc1b..8a61358d04521a 100644 --- a/x-pack/test/functional/services/cases/common.ts +++ b/x-pack/test/functional/services/cases/common.ts @@ -89,5 +89,17 @@ export function CasesCommonServiceProvider({ getService, getPageObject }: FtrPro } }); }, + + async setSearchTextInAssigneesPopover(text: string) { + await ( + await (await find.byClassName('euiContextMenuPanel')).findByClassName('euiFieldSearch') + ).type(text); + await header.waitUntilLoadingHasFinished(); + }, + + async selectFirstRowInAssigneesPopover() { + await (await find.byClassName('euiSelectableListItem__content')).click(); + await header.waitUntilLoadingHasFinished(); + }, }; } diff --git a/x-pack/test/functional/services/cases/create.ts b/x-pack/test/functional/services/cases/create.ts index e46201b1996c1e..872113f1a51be8 100644 --- a/x-pack/test/functional/services/cases/create.ts +++ b/x-pack/test/functional/services/cases/create.ts @@ -84,7 +84,7 @@ export function CasesCreateViewServiceProvider( }, async setCaseTags(tag: string) { - await comboBox.setCustom('comboBoxInput', tag); + await comboBox.setCustom('caseTags', tag); }, async assertCreateCaseFlyoutVisible(expectVisible = true) { diff --git a/x-pack/test/functional/services/cases/index.ts b/x-pack/test/functional/services/cases/index.ts index b4cfee637cb468..8ecabdac8c4c5d 100644 --- a/x-pack/test/functional/services/cases/index.ts +++ b/x-pack/test/functional/services/cases/index.ts @@ -20,7 +20,7 @@ export function CasesServiceProvider(context: FtrProviderContext) { return { api: CasesAPIServiceProvider(context), common: casesCommon, - casesTable: CasesTableServiceProvider(context), + casesTable: CasesTableServiceProvider(context, casesCommon), create: CasesCreateViewServiceProvider(context, casesCommon), navigation: CasesNavigationProvider(context), singleCase: CasesSingleViewServiceProvider(context), diff --git a/x-pack/test/functional/services/cases/list.ts b/x-pack/test/functional/services/cases/list.ts index 95b0a746db8ca4..a5f650198cf226 100644 --- a/x-pack/test/functional/services/cases/list.ts +++ b/x-pack/test/functional/services/cases/list.ts @@ -10,8 +10,12 @@ import { CaseStatuses } from '@kbn/cases-plugin/common'; import { CaseSeverityWithAll } from '@kbn/cases-plugin/common/ui'; import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { CasesCommon } from './common'; -export function CasesTableServiceProvider({ getService, getPageObject }: FtrProviderContext) { +export function CasesTableServiceProvider( + { getService, getPageObject }: FtrProviderContext, + casesCommon: CasesCommon +) { const common = getPageObject('common'); const testSubjects = getService('testSubjects'); const find = getService('find'); @@ -132,13 +136,11 @@ export function CasesTableServiceProvider({ getService, getPageObject }: FtrProv await testSubjects.click(`case-severity-filter-${severity}`); }, - async filterByReporter(reporter: string) { - await common.clickAndValidate( - 'options-filter-popover-button-Reporter', - `options-filter-popover-item-${reporter}` - ); + async filterByAssignee(assignee: string) { + await common.clickAndValidate('options-filter-popover-button-assignees', 'euiSelectableList'); - await testSubjects.click(`options-filter-popover-item-${reporter}`); + await casesCommon.setSearchTextInAssigneesPopover(assignee); + await casesCommon.selectFirstRowInAssigneesPopover(); }, async filterByOwner(owner: string) { diff --git a/x-pack/test/functional/services/cases/navigation.ts b/x-pack/test/functional/services/cases/navigation.ts index a54be7896877ea..8d3ba0e73a24ca 100644 --- a/x-pack/test/functional/services/cases/navigation.ts +++ b/x-pack/test/functional/services/cases/navigation.ts @@ -14,7 +14,7 @@ export function CasesNavigationProvider({ getPageObject, getService }: FtrProvid return { async navigateToApp(app: string = 'cases', appSelector: string = 'cases-app') { await common.navigateToApp(app); - await testSubjects.existOrFail(appSelector, { timeout: 2000 }); + await testSubjects.existOrFail(appSelector); }, async navigateToConfigurationPage(app: string = 'cases', appSelector: string = 'cases-app') { diff --git a/x-pack/test/functional/services/cases/single_case_view.ts b/x-pack/test/functional/services/cases/single_case_view.ts index 2db687f514778d..6bdd35ee642e54 100644 --- a/x-pack/test/functional/services/cases/single_case_view.ts +++ b/x-pack/test/functional/services/cases/single_case_view.ts @@ -107,5 +107,15 @@ export function CasesSingleViewServiceProvider({ getService, getPageObject }: Ft `Expected case description to be '${expectedDescription}' (got '${actualDescription}')` ); }, + + async openAssigneesPopover() { + await common.clickAndValidate('case-view-assignees-edit-button', 'euiSelectableList'); + await header.waitUntilLoadingHasFinished(); + }, + + async closeAssigneesPopover() { + await testSubjects.click('case-refresh'); + await header.waitUntilLoadingHasFinished(); + }, }; } diff --git a/x-pack/test/functional/services/ml/trained_models_table.ts b/x-pack/test/functional/services/ml/trained_models_table.ts index 03b0d961e1d4cf..c8d43207dd5ab9 100644 --- a/x-pack/test/functional/services/ml/trained_models_table.ts +++ b/x-pack/test/functional/services/ml/trained_models_table.ts @@ -284,7 +284,7 @@ export function TrainedModelsTableProvider( } public async openStartDeploymentModal(modelId: string) { - await testSubjects.clickWhenNotDisabledWithoutRetry( + await testSubjects.clickWhenNotDisabled( this.rowSelector(modelId, 'mlModelsTableRowStartDeploymentAction'), { timeout: 5000 } ); @@ -292,7 +292,7 @@ export function TrainedModelsTableProvider( } public async clickStopDeploymentAction(modelId: string) { - await testSubjects.clickWhenNotDisabledWithoutRetry( + await testSubjects.clickWhenNotDisabled( this.rowSelector(modelId, 'mlModelsTableRowStopDeploymentAction'), { timeout: 5000 } ); diff --git a/x-pack/test/functional/services/observability/alerts/common.ts b/x-pack/test/functional/services/observability/alerts/common.ts index 6491c7a8b0595b..a4b6cf579be883 100644 --- a/x-pack/test/functional/services/observability/alerts/common.ts +++ b/x-pack/test/functional/services/observability/alerts/common.ts @@ -36,6 +36,7 @@ export function ObservabilityAlertsCommonProvider({ const retry = getService('retry'); const toasts = getService('toasts'); const kibanaServer = getService('kibanaServer'); + const retryOnStale = getService('retryOnStale'); const navigateToTimeWithData = async () => { return await pageObjects.common.navigateToUrlWithBrowserHistory( @@ -55,10 +56,10 @@ export function ObservabilityAlertsCommonProvider({ ); }; - const navigateToAlertDetails = async (alertId: string) => { + const navigateToAlertDetails = async (alertId: string, ruleId: string) => { return await pageObjects.common.navigateToUrlWithBrowserHistory( 'observability', - `/alerts/${alertId}`, + `/alerts/rules/${ruleId}/alerts/${alertId}`, '', { ensureCurrentUrl: false } ); @@ -108,14 +109,14 @@ export function ObservabilityAlertsCommonProvider({ return await find.allByCssSelector('.euiDataGridRowCell input[type="checkbox"]:enabled'); }; - const getTableCellsInRows = async () => { + const getTableCellsInRows = retryOnStale.wrap(async () => { const columnHeaders = await getTableColumnHeaders(); if (columnHeaders.length <= 0) { return []; } const cells = await getTableCells(); return chunk(cells, columnHeaders.length); - }; + }); const getTableOrFail = async () => { return await testSubjects.existOrFail(ALERTS_TABLE_CONTAINER_SELECTOR); @@ -134,37 +135,28 @@ export function ObservabilityAlertsCommonProvider({ return await testSubjects.find('queryInput'); }; - const getQuerySubmitButton = async () => { - return await testSubjects.find('querySubmitButton'); - }; - - const clearQueryBar = async () => { + const clearQueryBar = retryOnStale.wrap(async () => { return await (await getQueryBar()).clearValueWithKeyboard(); - }; + }); - const typeInQueryBar = async (query: string) => { + const typeInQueryBar = retryOnStale.wrap(async (query: string) => { return await (await getQueryBar()).type(query); - }; + }); const submitQuery = async (query: string) => { await typeInQueryBar(query); - return await (await getQuerySubmitButton()).click(); + await testSubjects.click('querySubmitButton'); }; // Flyout - const getViewAlertDetailsFlyoutButton = async () => { + const openAlertsFlyout = retryOnStale.wrap(async () => { await openActionsMenuForRow(0); - - return await testSubjects.find('viewAlertDetailsFlyout'); - }; - - const openAlertsFlyout = async () => { - await (await getViewAlertDetailsFlyoutButton()).click(); + await testSubjects.click('viewAlertDetailsFlyout'); await retry.waitFor( 'flyout open', async () => await testSubjects.exists(ALERTS_FLYOUT_SELECTOR, { timeout: 2500 }) ); - }; + }); const getAlertsFlyout = async () => { return await testSubjects.find(ALERTS_FLYOUT_SELECTOR); @@ -190,15 +182,19 @@ export function ObservabilityAlertsCommonProvider({ return await testSubjects.existOrFail('viewRuleDetailsFlyout'); }; - const getAlertsFlyoutDescriptionListTitles = async (): Promise<WebElementWrapper[]> => { - const flyout = await getAlertsFlyout(); - return await testSubjects.findAllDescendant('alertsFlyoutDescriptionListTitle', flyout); - }; + const getAlertsFlyoutDescriptionListTitles = retryOnStale.wrap( + async (): Promise<WebElementWrapper[]> => { + const flyout = await getAlertsFlyout(); + return await testSubjects.findAllDescendant('alertsFlyoutDescriptionListTitle', flyout); + } + ); - const getAlertsFlyoutDescriptionListDescriptions = async (): Promise<WebElementWrapper[]> => { - const flyout = await getAlertsFlyout(); - return await testSubjects.findAllDescendant('alertsFlyoutDescriptionListDescription', flyout); - }; + const getAlertsFlyoutDescriptionListDescriptions = retryOnStale.wrap( + async (): Promise<WebElementWrapper[]> => { + const flyout = await getAlertsFlyout(); + return await testSubjects.findAllDescendant('alertsFlyoutDescriptionListDescription', flyout); + } + ); // Cell actions @@ -210,17 +206,19 @@ export function ObservabilityAlertsCommonProvider({ return await testSubjects.find(FILTER_FOR_VALUE_BUTTON_SELECTOR); }; - const openActionsMenuForRow = async (rowIndex: number) => { + const openActionsMenuForRow = retryOnStale.wrap(async (rowIndex: number) => { const actionsOverflowButton = await getActionsButtonByIndex(rowIndex); await actionsOverflowButton.click(); - }; + }); const viewRuleDetailsButtonClick = async () => { - return await (await testSubjects.find(VIEW_RULE_DETAILS_SELECTOR)).click(); + await testSubjects.click(VIEW_RULE_DETAILS_SELECTOR); }; + const viewRuleDetailsLinkClick = async () => { - return await (await testSubjects.find(VIEW_RULE_DETAILS_FLYOUT_SELECTOR)).click(); + await testSubjects.click(VIEW_RULE_DETAILS_FLYOUT_SELECTOR); }; + // Workflow status const setWorkflowStatusForRow = async (rowIndex: number, workflowStatus: WorkflowStatus) => { await openActionsMenuForRow(rowIndex); @@ -236,17 +234,14 @@ export function ObservabilityAlertsCommonProvider({ await toasts.dismissAllToasts(); }; - const setWorkflowStatusFilter = async (workflowStatus: WorkflowStatus) => { - const buttonGroupButton = await testSubjects.find( - `workflowStatusFilterButton-${workflowStatus}` - ); - await buttonGroupButton.click(); - }; + const setWorkflowStatusFilter = retryOnStale.wrap(async (workflowStatus: WorkflowStatus) => { + await testSubjects.click(`workflowStatusFilterButton-${workflowStatus}`); + }); - const getWorkflowStatusFilterValue = async () => { + const getWorkflowStatusFilterValue = retryOnStale.wrap(async () => { const selectedWorkflowStatusButton = await find.byClassName('euiButtonGroupButton-isSelected'); return await selectedWorkflowStatusButton.getVisibleText(); - }; + }); // Alert status const setAlertStatusFilter = async (alertStatus?: AlertStatus) => { @@ -257,8 +252,8 @@ export function ObservabilityAlertsCommonProvider({ if (alertStatus === ALERT_STATUS_RECOVERED) { buttonSubject = 'alert-status-filter-recovered-button'; } - const buttonGroupButton = await testSubjects.find(buttonSubject); - await buttonGroupButton.click(); + + await testSubjects.click(buttonSubject); }; const alertDataIsBeingLoaded = async () => { @@ -277,14 +272,12 @@ export function ObservabilityAlertsCommonProvider({ const isAbsoluteRange = await testSubjects.exists('superDatePickerstartDatePopoverButton'); if (isAbsoluteRange) { - const startButton = await testSubjects.find('superDatePickerstartDatePopoverButton'); - const endButton = await testSubjects.find('superDatePickerendDatePopoverButton'); - return `${await startButton.getVisibleText()} - ${await endButton.getVisibleText()}`; + const startText = await testSubjects.getVisibleText('superDatePickerstartDatePopoverButton'); + const endText = await testSubjects.getVisibleText('superDatePickerendDatePopoverButton'); + return `${startText} - ${endText}`; } - const datePickerButton = await testSubjects.find('superDatePickerShowDatesButton'); - const buttonText = await datePickerButton.getVisibleText(); - return buttonText; + return await testSubjects.getVisibleText('superDatePickerShowDatesButton'); }; const getActionsButtonByIndex = async (index: number) => { @@ -294,14 +287,14 @@ export function ObservabilityAlertsCommonProvider({ return actionsOverflowButtons[index] || null; }; - const getRuleStatValue = async (testSubj: string) => { + const getRuleStatValue = retryOnStale.wrap(async (testSubj: string) => { const stat = await testSubjects.find(testSubj); const title = await stat.findByCssSelector('.euiStat__title'); const count = await title.getVisibleText(); const value = Number.parseInt(count, 10); expect(Number.isNaN(value)).to.be(false); return value; - }; + }); return { getQueryBar, diff --git a/x-pack/test/functional/services/observability/index.ts b/x-pack/test/functional/services/observability/index.ts index 8990641cb524b1..2b2c82c4f3728c 100644 --- a/x-pack/test/functional/services/observability/index.ts +++ b/x-pack/test/functional/services/observability/index.ts @@ -8,13 +8,16 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { ObservabilityUsersProvider } from './users'; import { ObservabilityAlertsProvider } from './alerts'; +import { ObservabilityOverviewProvider } from './overview'; export function ObservabilityProvider(context: FtrProviderContext) { const alerts = ObservabilityAlertsProvider(context); const users = ObservabilityUsersProvider(context); + const overview = ObservabilityOverviewProvider(context); return { alerts, users, + overview, }; } diff --git a/x-pack/test/functional/services/observability/overview/common.ts b/x-pack/test/functional/services/observability/overview/common.ts new file mode 100644 index 00000000000000..e26e23e1e82db6 --- /dev/null +++ b/x-pack/test/functional/services/observability/overview/common.ts @@ -0,0 +1,89 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +// Based on the x-pack/test/functional/es_archives/observability/alerts archive. +const DATE_WITH_DATA = { + rangeFrom: '2021-10-18T13:36:22.109Z', + rangeTo: '2021-10-20T13:36:22.109Z', +}; + +const ALERTS_TITLE = 'Alerts'; +const ALERTS_ACCORDION_SELECTOR = `accordion-${ALERTS_TITLE}`; +const ALERTS_SECTION_BUTTON_SELECTOR = `button[aria-controls="${ALERTS_TITLE}"]`; +const ALERTS_TABLE_NO_DATA_SELECTOR = 'alertsStateTableEmptyState'; +const ALERTS_TABLE_WITH_DATA_SELECTOR = 'alertsTable'; +const ALERTS_TABLE_LOADING_SELECTOR = 'internalAlertsPageLoading'; + +export function ObservabilityOverviewCommonProvider({ + getPageObjects, + getService, +}: FtrProviderContext) { + const find = getService('find'); + const pageObjects = getPageObjects(['common']); + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + + const navigateToOverviewPageWithAlerts = async () => { + return await pageObjects.common.navigateToUrlWithBrowserHistory( + 'observability', + '/overview', + `?rangeFrom=${DATE_WITH_DATA.rangeFrom}&rangeTo=${DATE_WITH_DATA.rangeTo}`, + { ensureCurrentUrl: false } + ); + }; + + const navigateToOverviewPage = async () => { + return await pageObjects.common.navigateToUrlWithBrowserHistory( + 'observability', + '/overview', + undefined, + { ensureCurrentUrl: false } + ); + }; + + const waitForAlertsAccordionToAppear = async () => { + await retry.waitFor('alert accordion to appear', async () => { + return await testSubjects.exists(ALERTS_ACCORDION_SELECTOR); + }); + }; + + const waitForAlertsTableLoadingToDisappear = async () => { + await retry.try(async () => { + await testSubjects.missingOrFail(ALERTS_TABLE_LOADING_SELECTOR, { timeout: 10000 }); + }); + }; + + const openAlertsSection = async () => { + await waitForAlertsAccordionToAppear(); + const alertSectionButton = await find.byCssSelector(ALERTS_SECTION_BUTTON_SELECTOR); + return await alertSectionButton.click(); + }; + + const openAlertsSectionAndWaitToAppear = async () => { + await openAlertsSection(); + await waitForAlertsTableLoadingToDisappear(); + await retry.waitFor('alerts table to appear', async () => { + return ( + (await testSubjects.exists(ALERTS_TABLE_NO_DATA_SELECTOR)) || + (await testSubjects.exists(ALERTS_TABLE_WITH_DATA_SELECTOR)) + ); + }); + }; + + const getAlertsTableNoDataOrFail = async () => { + return await testSubjects.existOrFail(ALERTS_TABLE_NO_DATA_SELECTOR); + }; + + return { + getAlertsTableNoDataOrFail, + navigateToOverviewPageWithAlerts, + navigateToOverviewPage, + openAlertsSectionAndWaitToAppear, + }; +} diff --git a/x-pack/test/functional/services/observability/overview/index.ts b/x-pack/test/functional/services/observability/overview/index.ts new file mode 100644 index 00000000000000..5c34d4afce99e8 --- /dev/null +++ b/x-pack/test/functional/services/observability/overview/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ObservabilityOverviewCommonProvider } from './common'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export function ObservabilityOverviewProvider(context: FtrProviderContext) { + const common = ObservabilityOverviewCommonProvider(context); + + return { + common, + }; +} diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/common/users.ts b/x-pack/test/functional_with_es_ssl/apps/cases/common/users.ts index 282072dbb8dce5..8d213e5b780754 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/common/users.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/common/users.ts @@ -30,4 +30,10 @@ export const casesAllUser: User = { roles: [casesAll.name], }; -export const users = [casesReadDeleteUser, casesNoDeleteUser, casesAllUser]; +export const casesAllUser2: User = { + username: 'cases_all_user2', + password: 'password', + roles: [casesAll.name], +}; + +export const users = [casesReadDeleteUser, casesNoDeleteUser, casesAllUser, casesAllUser2]; diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts b/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts index a825bda9b90ee6..ec8e05ceb9b9d9 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts @@ -10,6 +10,11 @@ import { CaseStatuses } from '@kbn/cases-plugin/common'; import { CaseSeverity } from '@kbn/cases-plugin/common/api'; import { SeverityAll } from '@kbn/cases-plugin/common/ui'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { + createUsersAndRoles, + deleteUsersAndRoles, +} from '../../../cases_api_integration/common/lib/authentication'; +import { users, roles, casesAllUser, casesAllUser2 } from './common'; export default ({ getPageObject, getService }: FtrProviderContext) => { const header = getPageObject('header'); @@ -85,14 +90,20 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const caseTitle = 'matchme'; before(async () => { + await createUsersAndRoles(getService, users, roles); + await cases.api.activateUserProfiles([casesAllUser, casesAllUser2]); + + const profiles = await cases.api.suggestUserProfiles({ name: 'all', owners: ['cases'] }); + await cases.api.createCase({ title: caseTitle, tags: ['one'], description: 'lots of information about an incident', }); await cases.api.createCase({ title: 'test2', tags: ['two'] }); - await cases.api.createCase({ title: 'test3' }); - await cases.api.createCase({ title: 'test4' }); + await cases.api.createCase({ title: 'test3', assignees: [{ uid: profiles[0].uid }] }); + await cases.api.createCase({ title: 'test4', assignees: [{ uid: profiles[1].uid }] }); + await header.waitUntilLoadingHasFinished(); await cases.casesTable.waitForCasesToBeListed(); }); @@ -108,6 +119,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { after(async () => { await cases.api.deleteAllCases(); await cases.casesTable.waitForCasesToBeDeleted(); + await deleteUsersAndRoles(getService, users, roles); }); it('filters cases from the list using a full string match', async () => { @@ -186,19 +198,20 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await cases.casesTable.validateCasesTableHasNthRows(1); }); - /** - * TODO: Improve the test by creating a case from a - * different user and filter by the new user - * and not the default one - */ - it('filters cases by reporter', async () => { - await cases.casesTable.filterByReporter('elastic'); - await cases.casesTable.validateCasesTableHasNthRows(4); + it('filters cases by the first cases all user assignee', async () => { + await cases.casesTable.filterByAssignee('all'); + await cases.casesTable.validateCasesTableHasNthRows(1); + }); + + it('filters cases by the casesAllUser2 assignee', async () => { + await cases.casesTable.filterByAssignee('2'); + await cases.casesTable.validateCasesTableHasNthRows(1); }); }); describe('severity filtering', () => { before(async () => { + await cases.navigation.navigateToApp(); await cases.api.createCase({ severity: CaseSeverity.LOW }); await cases.api.createCase({ severity: CaseSeverity.LOW }); await cases.api.createCase({ severity: CaseSeverity.HIGH }); @@ -207,6 +220,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await header.waitUntilLoadingHasFinished(); await cases.casesTable.waitForCasesToBeListed(); }); + beforeEach(async () => { /** * There is no easy way to clear the filtering. diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts b/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts index 223127125e66d9..52540169a4c8d0 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts @@ -10,6 +10,11 @@ import uuid from 'uuid'; import { CaseStatuses } from '@kbn/cases-plugin/common'; import { CaseSeverity } from '@kbn/cases-plugin/common/api'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { + createUsersAndRoles, + deleteUsersAndRoles, +} from '../../../cases_api_integration/common/lib/authentication'; +import { users, roles, casesAllUser } from './common'; export default ({ getPageObject, getService }: FtrProviderContext) => { const header = getPageObject('header'); @@ -18,21 +23,12 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const cases = getService('cases'); const retry = getService('retry'); const comboBox = getService('comboBox'); + const security = getPageObject('security'); + const kibanaServer = getService('kibanaServer'); describe('View case', () => { describe('properties', () => { - // create the case to test on - before(async () => { - await cases.navigation.navigateToApp(); - await cases.api.createNthRandomCases(1); - await cases.casesTable.waitForCasesToBeListed(); - await cases.casesTable.goToFirstListedCase(); - await header.waitUntilLoadingHasFinished(); - }); - - after(async () => { - await cases.api.deleteAllCases(); - }); + createOneCaseBeforeDeleteAllAfter(getPageObject, getService); it('edits a case title from the case view page', async () => { const newTitle = `test-${uuid.v4()}`; @@ -167,18 +163,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); describe('actions', () => { - // create the case to test on - before(async () => { - await cases.navigation.navigateToApp(); - await cases.api.createNthRandomCases(1); - await cases.casesTable.waitForCasesToBeListed(); - await cases.casesTable.goToFirstListedCase(); - await header.waitUntilLoadingHasFinished(); - }); - - after(async () => { - await cases.api.deleteAllCases(); - }); + createOneCaseBeforeDeleteAllAfter(getPageObject, getService); it('deletes the case successfully', async () => { await cases.singleCase.deleteCase(); @@ -187,21 +172,12 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); describe('Severity field', () => { - before(async () => { - await cases.navigation.navigateToApp(); - await cases.api.createNthRandomCases(1); - await cases.casesTable.waitForCasesToBeListed(); - await cases.casesTable.goToFirstListedCase(); - await header.waitUntilLoadingHasFinished(); - }); - - after(async () => { - await cases.api.deleteAllCases(); - }); + createOneCaseBeforeDeleteAllAfter(getPageObject, getService); it('shows the severity field on the sidebar', async () => { await testSubjects.existOrFail('case-severity-selection'); }); + it('changes the severity level from the selector', async () => { await cases.common.selectSeverity(CaseSeverity.MEDIUM); await header.waitUntilLoadingHasFinished(); @@ -212,20 +188,128 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); }); - describe('Tabs', () => { - // create the case to test on + describe('Assignees field', () => { before(async () => { - await cases.navigation.navigateToApp(); - await cases.api.createNthRandomCases(1); - await cases.casesTable.waitForCasesToBeListed(); - await cases.casesTable.goToFirstListedCase(); - await header.waitUntilLoadingHasFinished(); + await createUsersAndRoles(getService, users, roles); + await cases.api.activateUserProfiles([casesAllUser]); }); after(async () => { - await cases.api.deleteAllCases(); + await deleteUsersAndRoles(getService, users, roles); + }); + + describe('unknown users', () => { + beforeEach(async () => { + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/cases/8.5.0/cases_assignees.json' + ); + + await cases.navigation.navigateToApp(); + await cases.casesTable.waitForCasesToBeListed(); + await cases.casesTable.goToFirstListedCase(); + await header.waitUntilLoadingHasFinished(); + }); + + afterEach(async () => { + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/cases/8.5.0/cases_assignees.json' + ); + + await cases.api.deleteAllCases(); + }); + + it('shows the unknown assignee', async () => { + await testSubjects.existOrFail('user-profile-assigned-user-group-abc'); + }); + + it('removes the unknown assignee when selecting the remove all users in the popover', async () => { + await testSubjects.existOrFail('user-profile-assigned-user-group-abc'); + + await cases.singleCase.openAssigneesPopover(); + await cases.common.setSearchTextInAssigneesPopover('case'); + await cases.common.selectFirstRowInAssigneesPopover(); + + await (await find.byButtonText('Remove all assignees')).click(); + await cases.singleCase.closeAssigneesPopover(); + await testSubjects.missingOrFail('user-profile-assigned-user-group-abc'); + }); + }); + + describe('login with cases all user', () => { + before(async () => { + await security.forceLogout(); + await security.login(casesAllUser.username, casesAllUser.password); + await createAndNavigateToCase(getPageObject, getService); + }); + + after(async () => { + await cases.api.deleteAllCases(); + await security.forceLogout(); + }); + + it('assigns the case to the current user when clicking the assign to self link', async () => { + await testSubjects.click('case-view-assign-yourself-link'); + await header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('user-profile-assigned-user-group-cases_all_user'); + }); }); + describe('logs in with default user', () => { + createOneCaseBeforeDeleteAllAfter(getPageObject, getService); + + afterEach(async () => { + await cases.singleCase.closeAssigneesPopover(); + }); + + it('shows the assign users popover when clicked', async () => { + await testSubjects.missingOrFail('euiSelectableList'); + + await cases.singleCase.openAssigneesPopover(); + }); + + it('assigns a user from the popover', async () => { + await cases.singleCase.openAssigneesPopover(); + await cases.common.setSearchTextInAssigneesPopover('case'); + await cases.common.selectFirstRowInAssigneesPopover(); + + // navigate out of the modal + await cases.singleCase.closeAssigneesPopover(); + await header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('user-profile-assigned-user-group-cases_all_user'); + }); + }); + + describe('logs in with default user and creates case before each', () => { + createOneCaseBeforeDeleteAllAfter(getPageObject, getService); + + it('removes an assigned user', async () => { + await cases.singleCase.openAssigneesPopover(); + await cases.common.setSearchTextInAssigneesPopover('case'); + await cases.common.selectFirstRowInAssigneesPopover(); + + // navigate out of the modal + await cases.singleCase.closeAssigneesPopover(); + await header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('user-profile-assigned-user-group-cases_all_user'); + + // hover over the assigned user + await ( + await find.byCssSelector( + '[data-test-subj="user-profile-assigned-user-group-cases_all_user"]' + ) + ).moveMouseTo(); + + // delete the user + await testSubjects.click('user-profile-assigned-user-cross-cases_all_user'); + + await testSubjects.existOrFail('case-view-assign-yourself-link'); + }); + }); + }); + + describe('Tabs', () => { + createOneCaseBeforeDeleteAllAfter(getPageObject, getService); + it('shows the "activity" tab by default', async () => { await testSubjects.existOrFail('case-view-tab-title-activity'); await testSubjects.existOrFail('case-view-tab-content-activity'); @@ -239,3 +323,32 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); }); }; + +const createOneCaseBeforeDeleteAllAfter = ( + getPageObject: FtrProviderContext['getPageObject'], + getService: FtrProviderContext['getService'] +) => { + const cases = getService('cases'); + + before(async () => { + await createAndNavigateToCase(getPageObject, getService); + }); + + after(async () => { + await cases.api.deleteAllCases(); + }); +}; + +const createAndNavigateToCase = async ( + getPageObject: FtrProviderContext['getPageObject'], + getService: FtrProviderContext['getService'] +) => { + const header = getPageObject('header'); + const cases = getService('cases'); + + await cases.navigation.navigateToApp(); + await cases.api.createNthRandomCases(1); + await cases.casesTable.waitForCasesToBeListed(); + await cases.casesTable.goToFirstListedCase(); + await header.waitUntilLoadingHasFinished(); +}; diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/cases/kibana.json b/x-pack/test/functional_with_es_ssl/fixtures/plugins/cases/kibana.json index 56e1f9fd62a08f..c7918341da86e5 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/cases/kibana.json +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/cases/kibana.json @@ -3,7 +3,7 @@ "owner": { "name": "Response Ops", "githubTeam": "response-ops" }, "version": "1.0.0", "kibanaVersion": "kibana", - "requiredPlugins": ["cases", "embeddable", "lens", "kibanaReact", "esUiShared"], + "requiredPlugins": ["cases", "embeddable", "lens", "kibanaReact", "esUiShared", "security"], "server": true, "ui": true } diff --git a/x-pack/test/observability_functional/apps/observability/index.ts b/x-pack/test/observability_functional/apps/observability/index.ts index b3acbf5f51a8a9..e797d6812ed8d7 100644 --- a/x-pack/test/observability_functional/apps/observability/index.ts +++ b/x-pack/test/observability_functional/apps/observability/index.ts @@ -10,13 +10,14 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('ObservabilityApp', function () { loadTestFile(require.resolve('./pages/alerts')); - loadTestFile(require.resolve('./pages/cases/case_details')); loadTestFile(require.resolve('./pages/alerts/add_to_case')); loadTestFile(require.resolve('./pages/alerts/alert_status')); loadTestFile(require.resolve('./pages/alerts/pagination')); loadTestFile(require.resolve('./pages/alerts/rule_stats')); loadTestFile(require.resolve('./pages/alerts/state_synchronization')); loadTestFile(require.resolve('./pages/alerts/table_storage')); + loadTestFile(require.resolve('./pages/cases/case_details')); + loadTestFile(require.resolve('./pages/overview/alert_table')); loadTestFile(require.resolve('./exploratory_view')); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./pages/rules_page')); diff --git a/x-pack/test/observability_functional/apps/observability/pages/alert_details_page.ts b/x-pack/test/observability_functional/apps/observability/pages/alert_details_page.ts index 472af7376d02ec..15479504bcb23e 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/alert_details_page.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alert_details_page.ts @@ -29,7 +29,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should show 404 page when the feature flag is disabled', async () => { - await observability.alerts.common.navigateToAlertDetails(uuid.v4()); + await observability.alerts.common.navigateToAlertDetails(uuid.v4(), uuid.v4()); await retry.waitFor( 'Alerts page to be visible', async () => await testSubjects.exists('pageNotFound') diff --git a/x-pack/test/observability_functional/apps/observability/pages/alerts/rule_stats.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/rule_stats.ts index 443e0616cabe24..15c960c16f7495 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/alerts/rule_stats.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/rule_stats.ts @@ -30,7 +30,7 @@ export default ({ getService }: FtrProviderContext) => { await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); const setup = async () => { await observability.alerts.common.setKibanaTimeZoneToUTC(); - await observability.alerts.common.navigateToTimeWithData(); + await observability.alerts.common.navigateWithoutFilter(); }; await setup(); }); diff --git a/x-pack/test/observability_functional/apps/observability/pages/cases/case_details.ts b/x-pack/test/observability_functional/apps/observability/pages/cases/case_details.ts index 340131c14b6a13..96e989a9173e7e 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/cases/case_details.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/cases/case_details.ts @@ -17,7 +17,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const find = getService('find'); const PageObjects = getPageObjects(['common', 'header']); - describe('Cases', () => { + describe('Observability cases', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); @@ -56,6 +56,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { after(async () => { await cases.api.deleteAllCases(); + await observability.users.restoreDefaultTestUserRole(); }); it('should link to observability rule pages in case details', async () => { diff --git a/x-pack/test/observability_functional/apps/observability/pages/overview/alert_table.ts b/x-pack/test/observability_functional/apps/observability/pages/overview/alert_table.ts new file mode 100644 index 00000000000000..c4dea3334bc3cc --- /dev/null +++ b/x-pack/test/observability_functional/apps/observability/pages/overview/alert_table.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +const ALL_ALERTS = 10; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const PageObjects = getPageObjects(['header']); + const esArchiver = getService('esArchiver'); + + // Failing: See https://github.com/elastic/kibana/issues/140507 + describe.skip('Observability overview', function () { + this.tags('includeFirefox'); + + const observability = getService('observability'); + const retry = getService('retry'); + + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts'); + }); + + describe('Without alerts', function () { + it('navigate and open alerts section', async () => { + await observability.overview.common.navigateToOverviewPage(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await observability.overview.common.openAlertsSectionAndWaitToAppear(); + }); + + it('should show no data message', async () => { + await retry.try(async () => { + await observability.overview.common.getAlertsTableNoDataOrFail(); + }); + }); + }); + + describe('With alerts', function () { + it('navigate and open alerts section', async () => { + await observability.overview.common.navigateToOverviewPageWithAlerts(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await observability.overview.common.openAlertsSectionAndWaitToAppear(); + }); + + it('should show alerts correctly', async () => { + await retry.try(async () => { + const tableRows = await observability.alerts.common.getTableCellsInRows(); + expect(tableRows.length).to.be(ALL_ALERTS); + }); + }); + }); + }); +}; diff --git a/x-pack/test/observability_functional/apps/observability/pages/rule_details_page.ts b/x-pack/test/observability_functional/apps/observability/pages/rule_details_page.ts index 6d17b9c6e09208..6ab449873fc767 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/rule_details_page.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/rule_details_page.ts @@ -45,7 +45,6 @@ export default ({ getService }: FtrProviderContext) => { const logThresholdRuleName = 'error-log'; before(async () => { - await observability.users.restoreDefaultTestUserRole(); const uptimeRule = { params: { search: '', @@ -82,6 +81,7 @@ export default ({ getService }: FtrProviderContext) => { uptimeRuleId = await createRule(uptimeRule); logThresholdRuleId = await createRule(logThresholdRule); }); + after(async () => { await deleteRuleById(uptimeRuleId); await deleteRuleById(logThresholdRuleId); diff --git a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts index a8b96c617db582..9ba2c69885b2a3 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts @@ -171,6 +171,8 @@ export default ({ getService }: FtrProviderContext) => { 'No permissions prompt', async () => await testSubjects.exists('noPermissionPrompt') ); + + await observability.users.restoreDefaultTestUserRole(); }); }); }); diff --git a/x-pack/test/performance/services/performance.ts b/x-pack/test/performance/services/performance.ts index 7f7156284d1860..ffddc834dc1157 100644 --- a/x-pack/test/performance/services/performance.ts +++ b/x-pack/test/performance/services/performance.ts @@ -8,10 +8,11 @@ /* eslint-disable no-console */ import Url from 'url'; +import * as Rx from 'rxjs'; import { inspect } from 'util'; import { setTimeout } from 'timers/promises'; import apm, { Span, Transaction } from 'elastic-apm-node'; -import playwright, { ChromiumBrowser, Page, BrowserContext, CDPSession } from 'playwright'; +import playwright, { ChromiumBrowser, Page, BrowserContext, CDPSession, Request } from 'playwright'; import { FtrService, FtrProviderContext } from '../ftr_provider_context'; export interface StepCtx { @@ -24,12 +25,16 @@ export type Steps = Array<{ name: string; handler: StepFn }>; export class PerformanceTestingService extends FtrService { private readonly auth = this.ctx.getService('auth'); + private readonly log = this.ctx.getService('log'); private readonly config = this.ctx.getService('config'); private browser: ChromiumBrowser | undefined; private currentSpanStack: Array<Span | null> = []; private currentTransaction: Transaction | undefined | null = undefined; + private pageTeardown$ = new Rx.Subject<Page>(); + private telemetryTrackerSubs = new Map<Page, Rx.Subscription>(); + constructor(ctx: FtrProviderContext) { super(ctx); @@ -164,6 +169,44 @@ export class PerformanceTestingService extends FtrService { return client; } + private telemetryTrackerCount = 0; + + private trackTelemetryRequests(page: Page) { + const id = ++this.telemetryTrackerCount; + + const requestFailure$ = Rx.fromEvent<Request>(page, 'requestfailed'); + const requestSuccess$ = Rx.fromEvent<Request>(page, 'requestfinished'); + const request$ = Rx.fromEvent<Request>(page, 'request').pipe( + Rx.takeUntil( + this.pageTeardown$.pipe( + Rx.first((p) => p === page), + Rx.delay(3000) + // If EBT client buffers: + // Rx.mergeMap(async () => { + // await page.waitForFunction(() => { + // // return window.kibana_ebt_client.buffer_size == 0 + // }); + // }) + ) + ), + Rx.mergeMap((request) => { + if (!request.url().includes('telemetry-staging.elastic.co')) { + return Rx.EMPTY; + } + + this.log.debug(`Waiting for telemetry request #${id} to complete`); + return Rx.merge(requestFailure$, requestSuccess$).pipe( + Rx.first((r) => r === request), + Rx.tap({ + complete: () => this.log.debug(`Telemetry request #${id} complete`), + }) + ); + }) + ); + + this.telemetryTrackerSubs.set(page, request$.subscribe()); + } + private async interceptBrowserRequests(page: Page) { await page.route('**', async (route, request) => { const headers = await request.allHeaders(); @@ -196,6 +239,7 @@ export class PerformanceTestingService extends FtrService { } const client = await this.sendCDPCommands(context, page); + this.trackTelemetryRequests(page); await this.interceptBrowserRequests(page); await this.handleSteps(steps, page); await this.tearDown(page, client, context); @@ -204,6 +248,16 @@ export class PerformanceTestingService extends FtrService { private async tearDown(page: Page, client: CDPSession, context: BrowserContext) { if (page) { + const telemetryTracker = this.telemetryTrackerSubs.get(page); + this.telemetryTrackerSubs.delete(page); + + if (telemetryTracker && !telemetryTracker.closed) { + this.log.info( + `Waiting for telemetry requests to complete, including requests starting within next 3 secs` + ); + this.pageTeardown$.next(page); + await new Promise<void>((resolve) => telemetryTracker.add(resolve)); + } await client.detach(); await page.close(); await context.close(); diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/migrations.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/migrations.ts index 04f34ab50a1331..232db750d54259 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/migrations.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/migrations.ts @@ -136,5 +136,59 @@ export default function createGetTests({ getService }: FtrProviderContext) { expect(response.body._source?.task.taskType).to.eql(`sampleTaskRemovedType`); expect(response.body._source?.task.status).to.eql(`unrecognized`); }); + + it('8.5.0 migrates active tasks to set enabled to true', async () => { + const response = await es.search<{ task: ConcreteTaskInstance }>( + { + index: '.kibana_task_manager', + size: 100, + body: { + query: { + match_all: {}, + }, + }, + }, + { + meta: true, + } + ); + expect(response.statusCode).to.eql(200); + const tasks = response.body.hits.hits; + tasks + .filter( + (task) => + task._source?.task.status !== 'failed' && task._source?.task.status !== 'unrecognized' + ) + .forEach((task) => { + expect(task._source?.task.enabled).to.eql(true); + }); + }); + + it('8.5.0 does not migrates failed and unrecognized', async () => { + const response = await es.search<{ task: ConcreteTaskInstance }>( + { + index: '.kibana_task_manager', + size: 100, + body: { + query: { + match_all: {}, + }, + }, + }, + { + meta: true, + } + ); + expect(response.statusCode).to.eql(200); + const tasks = response.body.hits.hits; + tasks + .filter( + (task) => + task._source?.task.status === 'failed' || task._source?.task.status === 'unrecognized' + ) + .forEach((task) => { + expect(task._source?.task.enabled).to.be(undefined); + }); + }); }); } diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts index cf720be74143ac..56477f0e6edf0f 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts @@ -11,7 +11,7 @@ import expect from '@kbn/expect'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import TaskManagerMapping from '@kbn/task-manager-plugin/server/saved_objects/mappings.json'; import { DEFAULT_POLL_INTERVAL } from '@kbn/task-manager-plugin/server/config'; -import { ConcreteTaskInstance, BulkUpdateSchedulesResult } from '@kbn/task-manager-plugin/server'; +import { ConcreteTaskInstance, BulkUpdateTaskResult } from '@kbn/task-manager-plugin/server'; import { FtrProviderContext } from '../../ftr_provider_context'; const { @@ -184,7 +184,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .send({ taskIds, schedule }) .expect(200) - .then((response: { body: BulkUpdateSchedulesResult }) => response.body); + .then((response: { body: BulkUpdateTaskResult }) => response.body); } // TODO: Add this back in with https://github.com/elastic/kibana/issues/106139 diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_browser_fields_by_feature_id.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_browser_fields_by_feature_id.ts new file mode 100644 index 00000000000000..6acbc14a473524 --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_browser_fields_by_feature_id.ts @@ -0,0 +1,71 @@ +/* + * 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 { superUser, obsOnlySpacesAll, secOnlyRead } from '../../../common/lib/authentication/users'; +import type { User } from '../../../common/lib/authentication/types'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + const SPACE1 = 'space1'; + const TEST_URL = '/internal/rac/alerts/browser_fields'; + + const getBrowserFieldsByFeatureId = async ( + user: User, + featureIds: string[], + expectedStatusCode: number = 200 + ) => { + const resp = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .query({ featureIds }) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(expectedStatusCode); + return resp.body; + }; + + describe('Alert - Get browser fields by featureId', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + describe('Users:', () => { + it(`${obsOnlySpacesAll.username} should be able to get browser fields for o11y featureIds`, async () => { + const browserFields = await getBrowserFieldsByFeatureId(obsOnlySpacesAll, [ + 'apm', + 'infrastructure', + 'logs', + 'uptime', + ]); + expect(Object.keys(browserFields)).to.eql(['base']); + }); + + it(`${superUser.username} should be able to get browser fields for o11y featureIds`, async () => { + const browserFields = await getBrowserFieldsByFeatureId(superUser, [ + 'apm', + 'infrastructure', + 'logs', + 'uptime', + ]); + expect(Object.keys(browserFields)).to.eql(['base']); + }); + + it(`${superUser.username} should NOT be able to get browser fields for siem featureId`, async () => { + await getBrowserFieldsByFeatureId(superUser, ['siem'], 404); + }); + + it(`${secOnlyRead.username} should NOT be able to get browser fields for siem featureId`, async () => { + await getBrowserFieldsByFeatureId(secOnlyRead, ['siem'], 404); + }); + }); + }); +}; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts index e96239f37cdfbf..e1046b2dca6d76 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts @@ -29,5 +29,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { loadTestFile(require.resolve('./get_alerts_index')); loadTestFile(require.resolve('./find_alerts')); loadTestFile(require.resolve('./search_strategy')); + loadTestFile(require.resolve('./get_browser_fields_by_feature_id')); }); }; diff --git a/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json b/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json index 3e468d7a84ca26..b10cd1b6a1c0d2 100644 --- a/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json @@ -1,174 +1,174 @@ { - "type":"doc", - "value":{ - "id":"a4cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f", - "index":"ml_host_risk_score_latest_default", - "source":{ - "@timestamp":"2021-03-10T14:51:05.766Z", - "risk_stats": { - "risk_score": 21, - "rule_risks": [ - { - "rule_name": "Unusual Linux Username", - "rule_risk": 42 - } - ] + "type": "doc", + "value": { + "id": "a4cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f", + "index": "ml_host_risk_score_latest_default", + "source": { + "@timestamp": "2021-03-10T14:51:05.766Z", + "host": { + "name": "siem-kibana", + "risk": { + "calculated_level": "Low", + "calculated_score_norm": 21, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + } }, - "host":{ - "name":"siem-kibana" - }, - "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", - "risk":"Low" + "ingest_timestamp": "2021-03-09T18:02:08.319296053Z" } } } { - "type":"doc", - "value":{ - "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb71f", - "index":"ml_host_risk_score_latest_default", - "source":{ - "@timestamp":"2021-03-10T14:51:05.766Z", - "risk_stats": { - "risk_score": 50, - "rule_risks": [ - { - "rule_name": "Unusual Linux Username", - "rule_risk": 42 - } - ] - }, - "host":{ - "name":"fake-1" + "type": "doc", + "value": { + "id": "a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb71f", + "index": "ml_host_risk_score_latest_default", + "source": { + "@timestamp": "2021-03-10T14:51:05.766Z", + "host": { + "name": "fake-1", + "risk": { + "calculated_level": "Moderate", + "calculated_score_norm": 50, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + } }, - "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", - "risk":"Moderate" + "ingest_timestamp": "2021-03-09T18:02:08.319296053Z" } } } { - "type":"doc", - "value":{ - "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb72f", - "index":"ml_host_risk_score_latest_default", - "source":{ - "@timestamp":"2021-03-10T14:51:05.766Z", - "risk_stats": { - "risk_score": 50, - "rule_risks": [ - { - "rule_name": "Unusual Linux Username", - "rule_risk": 42 - } - ] + "type": "doc", + "value": { + "id": "a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb72f", + "index": "ml_host_risk_score_latest_default", + "source": { + "@timestamp": "2021-03-10T14:51:05.766Z", + "host": { + "name": "fake-2", + "risk": { + "calculated_level": "Moderate", + "calculated_score_norm": 50, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + } }, - "host":{ - "name":"fake-2" - }, - "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", - "risk":"Moderate" + "ingest_timestamp": "2021-03-09T18:02:08.319296053Z" } } } { - "type":"doc", - "value":{ - "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb73f", - "index":"ml_host_risk_score_latest_default", - "source":{ - "@timestamp":"2021-03-10T14:51:05.766Z", - "risk_stats": { - "risk_score": 50, - "rule_risks": [ - { - "rule_name": "Unusual Linux Username", - "rule_risk": 42 - } - ] - }, - "host":{ - "name":"fake-3" + "type": "doc", + "value": { + "id": "a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb73f", + "index": "ml_host_risk_score_latest_default", + "source": { + "@timestamp": "2021-03-10T14:51:05.766Z", + "host": { + "name": "fake-3", + "risk": { + "calculated_level": "Moderate", + "calculated_score_norm": 50, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + } }, - "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", - "risk":"Moderate" + "ingest_timestamp": "2021-03-09T18:02:08.319296053Z" } } } { - "type":"doc", - "value":{ - "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f", - "index":"ml_host_risk_score_latest_default", - "source":{ - "@timestamp":"2021-03-10T14:51:05.766Z", - "risk_stats": { - "risk_score": 50, - "rule_risks": [ - { - "rule_name": "Unusual Linux Username", - "rule_risk": 42 - } - ] + "type": "doc", + "value": { + "id": "a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f", + "index": "ml_host_risk_score_latest_default", + "source": { + "@timestamp": "2021-03-10T14:51:05.766Z", + "host": { + "name": "fake-4", + "risk": { + "calculated_level": "Moderate", + "calculated_score_norm": 50, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + } }, - "host":{ - "name":"fake-4" - }, - "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", - "risk":"Moderate" + "ingest_timestamp": "2021-03-09T18:02:08.319296053Z" } } } { - "type":"doc", - "value":{ - "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb75f", - "index":"ml_host_risk_score_latest_default", - "source":{ - "@timestamp":"2021-03-10T14:51:05.766Z", - "risk_stats": { - "risk_score": 50, - "rule_risks": [ - { - "rule_name": "Unusual Linux Username", - "rule_risk": 42 - } - ] - }, - "host":{ - "name":"fake-5" + "type": "doc", + "value": { + "id": "a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb75f", + "index": "ml_host_risk_score_latest_default", + "source": { + "@timestamp": "2021-03-10T14:51:05.766Z", + "host": { + "name": "fake-5", + "risk": { + "calculated_level": "Moderate", + "calculated_score_norm": 50, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + } }, - "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", - "risk":"Moderate" + "ingest_timestamp": "2021-03-09T18:02:08.319296053Z" } } } { - "type":"doc", - "value":{ - "id":"a4cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f", - "index":"ml_host_risk_score_default", - "source":{ - "@timestamp":"2021-03-10T14:51:05.766Z", - "risk_stats": { - "risk_score": 21, - "rule_risks": [ - { - "rule_name": "Unusual Linux Username", - "rule_risk": 42 - } - ] - }, - "host":{ - "name":"siem-kibana" + "type": "doc", + "value": { + "id": "a4cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f", + "index": "ml_host_risk_score_default", + "source": { + "@timestamp": "2021-03-10T14:51:05.766Z", + "host": { + "name": "siem-kibana", + "risk": { + "calculated_level": "Low", + "calculated_score_norm": 21, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + } }, - "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", - "risk":"Low" + "ingest_timestamp": "2021-03-09T18:02:08.319296053Z" } } } diff --git a/x-pack/test/security_solution_cypress/es_archives/risky_hosts/mappings.json b/x-pack/test/security_solution_cypress/es_archives/risky_hosts/mappings.json index 02ceb5b5ebcccc..3e1b52cb22f5e9 100644 --- a/x-pack/test/security_solution_cypress/es_archives/risky_hosts/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/risky_hosts/mappings.json @@ -11,27 +11,21 @@ "properties": { "name": { "type": "keyword" + }, + "risk": { + "properties": { + "calculated_level": { + "type": "keyword" + }, + "calculated_score_norm": { + "type": "long" + } } } + } }, "ingest_timestamp": { "type": "date" - }, - "risk": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "risk_stats": { - "properties": { - "risk_score": { - "type": "long" - } - } } } }, @@ -69,35 +63,29 @@ "properties": { "name": { "type": "keyword" + }, + "risk": { + "properties": { + "calculated_level": { + "type": "keyword" + }, + "calculated_score_norm": { + "type": "long" + } } } + } }, "ingest_timestamp": { "type": "date" - }, - "risk": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "risk_stats": { - "properties": { - "risk_score": { - "type": "long" - } - } } } }, "settings": { "index": { "lifecycle": { - "name": "ml_host_risk_score_latest_default", - "rollover_alias": "ml_host_risk_score_latest_default" + "name": "ml_host_risk_score_default", + "rollover_alias": "ml_host_risk_score_default" }, "mapping": { "total_fields": { diff --git a/x-pack/test/security_solution_cypress/es_archives/risky_users/data.json b/x-pack/test/security_solution_cypress/es_archives/risky_users/data.json index 2ea72c8604dc6f..5cb0404a9d0d51 100644 --- a/x-pack/test/security_solution_cypress/es_archives/risky_users/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/risky_users/data.json @@ -1,174 +1,174 @@ { - "type":"doc", - "value":{ - "id":"a4cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f", - "index":"ml_user_risk_score_latest_default", - "source":{ - "@timestamp":"2021-03-10T14:51:05.766Z", - "risk_stats": { - "risk_score": 21, - "rule_risks": [ - { - "rule_name": "Unusual Linux Username", - "rule_risk": 42 - } - ] + "type": "doc", + "value": { + "id": "a4cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f", + "index": "ml_user_risk_score_latest_default", + "source": { + "@timestamp": "2021-03-10T14:51:05.766Z", + "user": { + "name": "user1", + "risk": { + "calculated_level": "Low", + "calculated_score_norm": 21, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + } }, - "user":{ - "name":"user1" - }, - "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", - "risk":"Low" + "ingest_timestamp": "2021-03-09T18:02:08.319296053Z" } } } { - "type":"doc", - "value":{ - "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb71f", - "index":"ml_user_risk_score_latest_default", - "source":{ - "@timestamp":"2021-03-10T14:51:05.766Z", - "risk_stats": { - "risk_score": 50, - "rule_risks": [ - { - "rule_name": "Unusual Linux Username", - "rule_risk": 42 - } - ] - }, - "user":{ - "name":"user2" + "type": "doc", + "value": { + "id": "a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb71f", + "index": "ml_user_risk_score_latest_default", + "source": { + "@timestamp": "2021-03-10T14:51:05.766Z", + "user": { + "name": "user2", + "risk": { + "calculated_score_norm": 50, + "calculated_level": "Moderate", + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + } }, - "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", - "risk":"Moderate" + "ingest_timestamp": "2021-03-09T18:02:08.319296053Z" } } } { - "type":"doc", - "value":{ - "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb72f", - "index":"ml_user_risk_score_latest_default", - "source":{ - "@timestamp":"2021-03-10T14:51:05.766Z", - "risk_stats": { - "risk_score": 50, - "rule_risks": [ - { - "rule_name": "Unusual Linux Username", - "rule_risk": 42 - } - ] + "type": "doc", + "value": { + "id": "a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb72f", + "index": "ml_user_risk_score_latest_default", + "source": { + "@timestamp": "2021-03-10T14:51:05.766Z", + "user": { + "name": "user3", + "risk": { + "calculated_score_norm": 50, + "calculated_level": "Moderate", + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + } }, - "user":{ - "name":"user3" - }, - "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", - "risk":"Moderate" + "ingest_timestamp": "2021-03-09T18:02:08.319296053Z" } } } { - "type":"doc", - "value":{ - "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb73f", - "index":"ml_user_risk_score_latest_default", - "source":{ - "@timestamp":"2021-03-10T14:51:05.766Z", - "risk_stats": { - "risk_score": 50, - "rule_risks": [ - { - "rule_name": "Unusual Linux Username", - "rule_risk": 42 - } - ] - }, - "user":{ - "name":"user4" + "type": "doc", + "value": { + "id": "a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb73f", + "index": "ml_user_risk_score_latest_default", + "source": { + "@timestamp": "2021-03-10T14:51:05.766Z", + "user": { + "name": "user4", + "risk": { + "calculated_score_norm": 50, + "calculated_level": "Moderate", + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + } }, - "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", - "risk":"Moderate" + "ingest_timestamp": "2021-03-09T18:02:08.319296053Z" } } } { - "type":"doc", - "value":{ - "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f", - "index":"ml_user_risk_score_latest_default", - "source":{ - "@timestamp":"2021-03-10T14:51:05.766Z", - "risk_stats": { - "risk_score": 50, - "rule_risks": [ - { - "rule_name": "Unusual Linux Username", - "rule_risk": 42 - } - ] + "type": "doc", + "value": { + "id": "a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f", + "index": "ml_user_risk_score_latest_default", + "source": { + "@timestamp": "2021-03-10T14:51:05.766Z", + "user": { + "name": "user5", + "risk": { + "calculated_score_norm": 50, + "calculated_level": "Moderate", + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + } }, - "user":{ - "name":"user5" - }, - "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", - "risk":"Moderate" + "ingest_timestamp": "2021-03-09T18:02:08.319296053Z" } } } { - "type":"doc", - "value":{ - "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb75f", - "index":"ml_user_risk_score_latest_default", - "source":{ - "@timestamp":"2021-03-10T14:51:05.766Z", - "risk_stats": { - "risk_score": 50, - "rule_risks": [ - { - "rule_name": "Unusual Linux Username", - "rule_risk": 42 - } - ] - }, - "user":{ - "name":"user6" + "type": "doc", + "value": { + "id": "a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb75f", + "index": "ml_user_risk_score_latest_default", + "source": { + "@timestamp": "2021-03-10T14:51:05.766Z", + "user": { + "name": "user6", + "risk": { + "calculated_score_norm": 50, + "calculated_level": "Moderate", + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + } }, - "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", - "risk":"Moderate" + "ingest_timestamp": "2021-03-09T18:02:08.319296053Z" } } } { - "type":"doc", - "value":{ - "id":"a4cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f", - "index":"ml_user_risk_score_default", - "source":{ - "@timestamp":"2021-03-10T14:51:05.766Z", - "risk_stats": { - "risk_score": 21, - "rule_risks": [ - { - "rule_name": "Unusual Linux Username", - "rule_risk": 42 - } - ] + "type": "doc", + "value": { + "id": "a4cf452c1e0375c3d4412cb550bd1783358468b3b3b777da4829d72c7d6fb74f", + "index": "ml_user_risk_score_default", + "source": { + "@timestamp": "2021-03-10T14:51:05.766Z", + "user": { + "name": "user1", + "risk": { + "calculated_score_norm": 21, + "calculated_level": "Low", + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + } }, - "user":{ - "name":"user7" - }, - "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", - "risk":"Low" + "ingest_timestamp": "2021-03-09T18:02:08.319296053Z" } } -} +} \ No newline at end of file diff --git a/x-pack/test/security_solution_cypress/es_archives/risky_users/mappings.json b/x-pack/test/security_solution_cypress/es_archives/risky_users/mappings.json index 6e8db71b1813de..77eade9df7994c 100644 --- a/x-pack/test/security_solution_cypress/es_archives/risky_users/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/risky_users/mappings.json @@ -11,27 +11,21 @@ "properties": { "name": { "type": "keyword" + }, + "risk": { + "properties": { + "calculated_level": { + "type": "keyword" + }, + "calculated_score_norm": { + "type": "long" + } } } + } }, "ingest_timestamp": { "type": "date" - }, - "risk": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "risk_stats": { - "properties": { - "risk_score": { - "type": "long" - } - } } } }, @@ -69,35 +63,29 @@ "properties": { "name": { "type": "keyword" + }, + "risk": { + "properties": { + "calculated_level": { + "type": "keyword" + }, + "calculated_score_norm": { + "type": "long" + } } } + } }, "ingest_timestamp": { "type": "date" - }, - "risk": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "risk_stats": { - "properties": { - "risk_score": { - "type": "long" - } - } } } }, "settings": { "index": { "lifecycle": { - "name": "ml_user_risk_score_latest_default", - "rollover_alias": "ml_user_risk_score_latest_default" + "name": "ml_user_risk_score_default", + "rollover_alias": "ml_user_risk_score_default" }, "mapping": { "total_fields": { @@ -111,4 +99,4 @@ } } } -} +} \ No newline at end of file diff --git a/x-pack/test/security_solution_cypress/runner.ts b/x-pack/test/security_solution_cypress/runner.ts index 79e202046a4e06..08dc0f949e01c6 100644 --- a/x-pack/test/security_solution_cypress/runner.ts +++ b/x-pack/test/security_solution_cypress/runner.ts @@ -17,10 +17,7 @@ import semver from 'semver'; import { FtrProviderContext } from './ftr_provider_context'; const retrieveIntegrations = (chunksTotal: number, chunkIndex: number) => { - const pattern = resolve( - __dirname, - '../../plugins/security_solution/cypress/integration/**/*.spec.ts' - ); + const pattern = resolve(__dirname, '../../plugins/security_solution/cypress/e2e/**/*.cy.ts'); const integrationsPaths = globby.sync(pattern); const chunkSize = Math.ceil(integrationsPaths.length / chunksTotal); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index 81a1dc109b562d..a5b13c083b278f 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -34,28 +34,38 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'Actions', ], [ - 'Host-ku5jy6j0pw', + 'Host-dpu1a2r2yi', 'x', 'x', - 'Unsupported', - 'Windows', - '10.12.215.130, 10.130.188.228,10.19.102.141', - '7.0.13', + 'Warning', + 'Linux', + '10.2.17.24, 10.56.215.200,10.254.196.130', + '8.5.0', 'x', '', ], [ - 'Host-ntr4rkj24m', + 'Host-rs9wp4o6l9', 'x', 'x', 'Success', - 'Windows', - '10.36.46.252, 10.222.152.110', - '7.4.13', + 'Linux', + '10.138.79.131, 10.170.160.154', + '8.5.0', + 'x', + '', + ], + [ + 'Host-u5jy6j0pwb', + 'x', + 'x', + 'Warning', + 'Linux', + '10.87.11.145, 10.117.106.109,10.242.136.97', + '8.5.0', 'x', '', ], - ['Host-q9qenwrl9k', 'x', 'x', 'Warning', 'Windows', '10.206.226.90', '7.11.10', 'x', ''], ]; const formattedTableData = async () => { @@ -183,38 +193,16 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(tableData).to.eql(expectedDataFromQuery); }); - it('for the kql filtering for united.endpoint.host.hostname : "Host-ku5jy6j0pw", table shows 1 item', async () => { + it('for the kql filtering for united.endpoint.host.hostname, table shows 1 item', async () => { + const expectedDataFromQuery = [...expectedData.slice(0, 2).map((row) => [...row])]; + const hostName = expectedDataFromQuery[1][0]; const adminSearchBar = await testSubjects.find('adminSearchBar'); await adminSearchBar.clearValueWithKeyboard(); await adminSearchBar.type( - 'united.endpoint.host.hostname : "Host-ku5jy6j0pw" or host.hostname : "Host-ku5jy6j0pw" ' + `united.endpoint.host.hostname : "${hostName}" or host.hostname : "${hostName}" ` ); const querySubmitButton = await testSubjects.find('querySubmitButton'); await querySubmitButton.click(); - const expectedDataFromQuery = [ - [ - 'Endpoint', - 'Agent status', - 'Policy', - 'Policy status', - 'OS', - 'IP address', - 'Version', - 'Last active', - 'Actions', - ], - [ - 'Host-ku5jy6j0pw', - 'x', - 'x', - 'Unsupported', - 'Windows', - '10.12.215.130, 10.130.188.228,10.19.102.141', - '7.0.13', - 'x', - '', - ], - ]; await pageObjects.endpoint.waitForTableToHaveNumberOfEntries( 'endpointListTable', 1, diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_solution_integrations.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_solution_integrations.ts new file mode 100644 index 00000000000000..d9ff1179f62a9a --- /dev/null +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_solution_integrations.ts @@ -0,0 +1,103 @@ +/* + * 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 { IndexedHostsAndAlertsResponse } from '@kbn/security-solution-plugin/common/endpoint/index_data'; +import { TimelineResponse } from '@kbn/security-solution-plugin/common/types'; +import { kibanaPackageJson } from '@kbn/utils'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +/** + * Test suite is meant to cover usages of endpoint functionality or access to endpoint + * functionality from other areas of security solution. + */ +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const endpointService = getService('endpointTestResources'); + const timelineTestService = getService('timeline'); + const detectionsTestService = getService('detections'); + const log = getService('log'); + const testSubjects = getService('testSubjects'); + const pageObjects = getPageObjects(['common', 'timeline']); + + describe('App level Endpoint functionality', () => { + let indexedData: IndexedHostsAndAlertsResponse; + let endpointAgentId: string; + + before(async () => { + indexedData = await endpointService.loadEndpointData({ + numHosts: 2, + generatorSeed: `app-level-endpoint-${Math.random()}`, + }); + + endpointAgentId = indexedData.hosts[0].agent.id; + + await endpointService.waitForUnitedEndpoints([endpointAgentId]); + + // Ensure our Endpoint is for v8.0 (or whatever is running in kibana now) + await endpointService.sendEndpointMetadataUpdate(endpointAgentId, { + agent: { version: kibanaPackageJson.version }, + }); + + // start/stop the endpoint rule. This should cause the rule to run immediately + // and avoid us having to wait for the interval (of 5 minutes) + await detectionsTestService.stopStartEndpointRule(); + }); + + after(async () => { + if (indexedData) { + log.info('Cleaning up loaded endpoint data'); + await endpointService.unloadEndpointData(indexedData); + } + }); + + describe('from Timeline', () => { + let timeline: TimelineResponse; + + before(async () => { + timeline = await timelineTestService.createTimelineForEndpointAlerts( + 'endpoint in timeline', + { + endpointAgentId, + } + ); + + // wait for alerts to be available for the Endpoint ID + await detectionsTestService.waitForAlerts( + timelineTestService.getEndpointAlertsKqlQuery(endpointAgentId).esQuery, + // The Alerts rules seems to run every 5 minutes, so we wait here a max + // of 6 minutes to ensure it runs and completes and alerts are available. + 60_000 * 6 + ); + + await pageObjects.timeline.navigateToTimelineList(); + await pageObjects.timeline.openTimelineById( + timeline.data.persistTimeline.timeline.savedObjectId + ); + await pageObjects.timeline.setDateRange('Last 1 year'); + await pageObjects.timeline.waitForEvents(60_000); + }); + + after(async () => { + if (timeline) { + log.info( + `Cleaning up created timeline [${timeline.data.persistTimeline.timeline.title} - ${timeline.data.persistTimeline.timeline.savedObjectId}]` + ); + await timelineTestService.deleteTimeline( + timeline.data.persistTimeline.timeline.savedObjectId + ); + } + }); + + it('should show Isolation action in alert details', async () => { + await pageObjects.timeline.showEventDetails(); + await testSubjects.click('take-action-dropdown-btn'); + await testSubjects.clickWhenNotDisabled('isolate-host-action-item'); + await testSubjects.existOrFail('endpointHostIsolationForm'); + await testSubjects.click('hostIsolateCancelButton'); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts index 84f90830a3f14f..6aab2457c52788 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts @@ -43,5 +43,6 @@ export default function (providerContext: FtrProviderContext) { loadTestFile(require.resolve('./endpoint_permissions')); loadTestFile(require.resolve('./artifact_entries_list')); loadTestFile(require.resolve('./responder')); + loadTestFile(require.resolve('./endpoint_solution_integrations')); }); } diff --git a/x-pack/test/security_solution_endpoint/services/endpoint.ts b/x-pack/test/security_solution_endpoint/services/endpoint.ts index 50e0b0821ec81d..e29ae10a60df84 100644 --- a/x-pack/test/security_solution_endpoint/services/endpoint.ts +++ b/x-pack/test/security_solution_endpoint/services/endpoint.ts @@ -5,6 +5,8 @@ * 2.0. */ +/* eslint-disable max-classes-per-file */ + import { errors } from '@elastic/elasticsearch'; import { Client } from '@elastic/elasticsearch'; import { @@ -12,6 +14,8 @@ import { metadataTransformPrefix, METADATA_UNITED_INDEX, METADATA_UNITED_TRANSFORM, + HOST_METADATA_GET_ROUTE, + METADATA_DATASTREAM, } from '@kbn/security-solution-plugin/common/endpoint/constants'; import { deleteIndexedHostsAndAlerts, @@ -24,14 +28,37 @@ import { catchAndWrapError } from '@kbn/security-solution-plugin/server/endpoint import { installOrUpgradeEndpointFleetPackage } from '@kbn/security-solution-plugin/common/endpoint/data_loaders/setup_fleet_for_endpoint'; import { EndpointError } from '@kbn/security-solution-plugin/common/endpoint/errors'; import { STARTED_TRANSFORM_STATES } from '@kbn/security-solution-plugin/common/constants'; +import { DeepPartial } from 'utility-types'; +import { HostInfo, HostMetadata } from '@kbn/security-solution-plugin/common/endpoint/types'; +import { EndpointDocGenerator } from '@kbn/security-solution-plugin/common/endpoint/generate_data'; +import { EndpointMetadataGenerator } from '@kbn/security-solution-plugin/common/endpoint/data_generators/endpoint_metadata_generator'; +import { merge } from 'lodash'; +import { kibanaPackageJson } from '@kbn/utils'; +import seedrandom from 'seedrandom'; import { FtrService } from '../../functional/ftr_provider_context'; +// Document Generator override that uses a custom Endpoint Metadata generator and sets the +// `agent.version` to the current version +const CurrentKibanaVersionDocGenerator = class extends EndpointDocGenerator { + constructor(seedValue: string | seedrandom.prng) { + const MetadataGenerator = class extends EndpointMetadataGenerator { + protected randomVersion(): string { + return kibanaPackageJson.version; + } + }; + + super(seedValue, MetadataGenerator); + } +}; + export class EndpointTestResources extends FtrService { private readonly esClient = this.ctx.getService('es'); private readonly retry = this.ctx.getService('retry'); private readonly kbnClient = this.ctx.getService('kibanaServer'); private readonly transform = this.ctx.getService('transform'); private readonly config = this.ctx.getService('config'); + private readonly supertest = this.ctx.getService('supertest'); + private readonly log = this.ctx.getService('log'); private generateTransformId(endpointPackageVersion?: string): string { return `${metadataTransformPrefix}-${endpointPackageVersion ?? ''}`; @@ -159,7 +186,9 @@ export class EndpointTestResources extends FtrService { 'logs-endpoint.events.process-default', 'logs-endpoint.alerts-default', alertsPerHost, - enableFleetIntegration + enableFleetIntegration, + undefined, + CurrentKibanaVersionDocGenerator ); if (waitUntilTransformed) { @@ -291,4 +320,67 @@ export class EndpointTestResources extends FtrService { > { return installOrUpgradeEndpointFleetPackage(this.kbnClient); } + + /** + * Fetch (GET) the details of an endpoint + * @param endpointAgentId + */ + async fetchEndpointMetadata(endpointAgentId: string): Promise<HostInfo> { + const metadata = this.supertest + .get(HOST_METADATA_GET_ROUTE.replace('{id}', endpointAgentId)) + .set('kbn-xsrf', 'true') + .send() + .expect(200) + .then((response) => response.body as HostInfo); + + return metadata; + } + + /** + * Sends an updated metadata document for a given endpoint to the datastream and waits for the + * update to show up on the Metadata API (after transform runs) + */ + async sendEndpointMetadataUpdate( + endpointAgentId: string, + updates: DeepPartial<HostMetadata> = {} + ): Promise<HostInfo> { + const currentMetadata = await this.fetchEndpointMetadata(endpointAgentId); + const generatedMetadataDoc = new EndpointDocGenerator().generateHostMetadata(); + + const updatedMetadataDoc = merge( + { ...currentMetadata.metadata }, + // Grab the updated `event` and timestamp from the generator data + { + event: generatedMetadataDoc.event, + '@timestamp': generatedMetadataDoc['@timestamp'], + }, + updates + ); + + await this.esClient.index({ + index: METADATA_DATASTREAM, + body: updatedMetadataDoc, + op_type: 'create', + }); + + let response: HostInfo | undefined; + + // Wait for the update to show up on Metadata API (after transform runs) + await this.retry.waitFor( + `Waiting for update to endpoint id [${endpointAgentId}] to be processed by transform`, + async () => { + response = await this.fetchEndpointMetadata(endpointAgentId); + + return response.metadata.event.id === updatedMetadataDoc.event.id; + } + ); + + if (!response) { + throw new Error(`Response object not set. Issue fetching endpoint metadata`); + } + + this.log.info(`Endpoint metadata doc update done: \n${JSON.stringify(response)}`); + + return response; + } } diff --git a/x-pack/test/security_solution_ftr/services/timeline/index.ts b/x-pack/test/security_solution_ftr/services/timeline/index.ts index 5c6e96b4ce0be8..38e7d2a667cd29 100644 --- a/x-pack/test/security_solution_ftr/services/timeline/index.ts +++ b/x-pack/test/security_solution_ftr/services/timeline/index.ts @@ -11,6 +11,7 @@ import { TIMELINE_DRAFT_URL, TIMELINE_URL } from '@kbn/security-solution-plugin/ import { TimelineResponse } from '@kbn/security-solution-plugin/common/types'; import { TimelineInput } from '@kbn/security-solution-plugin/common/search_strategy'; import moment from 'moment'; +import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { FtrService } from '../../../functional/ftr_provider_context'; export class TimelineTestService extends FtrService { @@ -67,10 +68,9 @@ export class TimelineTestService extends FtrService { this.log.info(JSON.stringify(createdTimeline)); }); - const { savedObjectId: timelineId, version, ...timelineDoc } = createdTimeline; + const { savedObjectId: timelineId, version } = createdTimeline; const timelineUpdate: TimelineInput = { - ...(timelineDoc as TimelineInput), title, // Set date range to the last 1 year dateRange: { @@ -109,9 +109,6 @@ export class TimelineTestService extends FtrService { version: string ): Promise<TimelineResponse> { return await this.supertest - // DEV NOTE/FYI: - // Although this API is a `patch`, it does not seem that it actually does a patch, - // so `updates` should always be the full timeline record .patch(TIMELINE_URL) .set('kbn-xsrf', 'true') .send({ @@ -134,4 +131,66 @@ export class TimelineTestService extends FtrService { .then(this.getHttpResponseFailureHandler()) .then((response) => response.body as TimelineResponse); } + + /** + * Get the KQL query that will filter the content of a timeline to display Endpoint alerts + * @param endpointAgentId + */ + getEndpointAlertsKqlQuery(endpointAgentId?: string): { + expression: string; + esQuery: ReturnType<typeof toElasticsearchQuery>; + } { + const expression = [ + 'agent.type: "endpoint"', + 'kibana.alert.rule.uuid : *', + ...(endpointAgentId ? [`agent.id: "${endpointAgentId}"`] : []), + ].join(' AND '); + + const esQuery = toElasticsearchQuery(fromKueryExpression(expression)); + + return { + expression, + esQuery, + }; + } + + /** + * Crates a new Timeline and sets its `kqlQuery` so that Endpoint Alerts are displayed. + * Can be limited to an endpoint by providing its `agent.id` + * + * @param title + * @param endpointAgentId + */ + async createTimelineForEndpointAlerts( + title: string, + { + endpointAgentId, + }: Partial<{ + /** If defined, then only alerts from the specific `agent.id` will be displayed */ + endpointAgentId: string; + }> + ): Promise<TimelineResponse> { + const newTimeline = await this.createTimeline(title); + + const { expression, esQuery } = this.getEndpointAlertsKqlQuery(endpointAgentId); + + const updatedTimeline = await this.updateTimeline( + newTimeline.data.persistTimeline.timeline.savedObjectId, + { + title, + kqlQuery: { + filterQuery: { + kuery: { + kind: 'kuery', + expression, + }, + serializedQuery: JSON.stringify(esQuery), + }, + }, + }, + newTimeline.data.persistTimeline.timeline.version + ); + + return updatedTimeline; + } } diff --git a/x-pack/test/session_view/basic/tests/process_events_route.ts b/x-pack/test/session_view/basic/tests/process_events_route.ts index c5495248c47144..abb463c72af25d 100644 --- a/x-pack/test/session_view/basic/tests/process_events_route.ts +++ b/x-pack/test/session_view/basic/tests/process_events_route.ts @@ -48,11 +48,13 @@ export default function processEventsTests({ getService }: FtrProviderContext) { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/session_view/process_events'); await esArchiver.load('x-pack/test/functional/es_archives/session_view/alerts'); + await esArchiver.load('x-pack/test/functional/es_archives/session_view/io_events'); }); after(async () => { await esArchiver.unload('x-pack/test/functional/es_archives/session_view/process_events'); await esArchiver.unload('x-pack/test/functional/es_archives/session_view/alerts'); + await esArchiver.unload('x-pack/test/functional/es_archives/session_view/io_events'); }); it(`${PROCESS_EVENTS_ROUTE} returns a page of process events`, async () => { diff --git a/x-pack/test/spaces_api_integration/common/config.ts b/x-pack/test/spaces_api_integration/common/config.ts index 15a63fec6d309a..d7139e7bd1c343 100644 --- a/x-pack/test/spaces_api_integration/common/config.ts +++ b/x-pack/test/spaces_api_integration/common/config.ts @@ -41,6 +41,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) retry: config.xpack.api.get('services.retry'), esArchiver: config.kibana.functional.get('services.esArchiver'), kibanaServer: config.kibana.functional.get('services.kibanaServer'), + spaces: config.xpack.api.get('services.spaces'), }, junit: { reportName: 'X-Pack Spaces API Integration Tests -- ' + name, diff --git a/x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/default_space.json b/x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/default_space.json new file mode 100644 index 00000000000000..9179a846066f15 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/default_space.json @@ -0,0 +1,330 @@ +{ + "attributes": { + "title": "Copy to Space index pattern 1 from default space" + }, + "coreMigrationVersion": "8.4.0", + "id": "cts_ip_1_default", + "migrationVersion": { + "index-pattern": "8.0.0" + }, + "originId": "cts_ip_1", + "references": [], + "type": "index-pattern", + "updated_at": "2017-09-21T18:49:16.270Z", + "version": "WzUyOCwxXQ==" +} + +{ + "attributes": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "CTS vis 1 from default space", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{},\"legendSize\":\"auto\"},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}" + }, + "coreMigrationVersion": "8.4.0", + "id": "cts_vis_1_default", + "migrationVersion": { + "visualization": "8.3.0" + }, + "references": [ + { + "id": "cts_ip_1_default", + "name": "CTS IP 1", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2017-09-21T18:49:16.270Z", + "version": "WzUyMSwxXQ==" +} + +{ + "attributes": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "CTS vis 2 from default space", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{},\"legendSize\":\"auto\"},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}" + }, + "coreMigrationVersion": "8.4.0", + "id": "cts_vis_2_default", + "migrationVersion": { + "visualization": "8.3.0" + }, + "references": [ + { + "id": "cts_ip_1_default", + "name": "CTS IP 1", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2017-09-21T18:49:16.270Z", + "version": "WzUyMiwxXQ==" +} + +{ + "attributes": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "CTS vis 3 from default space", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{},\"legendSize\":\"auto\"},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}" + }, + "coreMigrationVersion": "8.4.0", + "id": "cts_vis_3_default", + "migrationVersion": { + "visualization": "8.3.0" + }, + "originId": "cts_vis_3", + "references": [ + { + "id": "cts_ip_1_default", + "name": "CTS IP 1", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2017-09-21T18:49:16.270Z", + "version": "WzUyMywxXQ==" +} + +{ + "attributes": { + "description": "Copy to Space Dashboard from the default space", + "title": "This is the default test space CTS dashboard" + }, + "coreMigrationVersion": "8.4.0", + "id": "cts_dashboard_default", + "migrationVersion": { + "dashboard": "8.4.0" + }, + "originId": "cts_dashboard", + "references": [ + { + "id": "cts_vis_1_default", + "name": "CTS Vis 1", + "type": "visualization" + }, + { + "id": "cts_vis_2_default", + "name": "CTS Vis 2", + "type": "visualization" + }, + { + "id": "cts_vis_3_default", + "name": "CTS Vis 3", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2017-09-21T18:49:16.270Z", + "version": "WzUyMCwxXQ==" +} + +{ + "attributes": { + "title": "A shared saved-object in one space" + }, + "id": "conflict_2_default", + "originId": "conflict_2", + "references": [], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzUxMCwxXQ==" +} + +{ + "attributes": { + "title": "A shared saved-object in all spaces" + }, + "id": "conflict_2_all", + "originId": "conflict_2", + "references": [], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzUxMSwxXQ==" +} + +{ + "attributes": { + "title": "This is used to test an inexact match conflict for an originId -> id match" + }, + "id": "conflict_1b_default", + "originId": "conflict_1b_space_2", + "references": [], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzUwMywxXQ==" +} + +{ + "attributes": { + "title": "This is used to test an inexact match conflict for an originId -> originId match" + }, + "id": "conflict_1a_default", + "originId": "conflict_1a", + "references": [], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzUwMCwxXQ==" +} + +{ + "attributes": { + "title": "Some title" + }, + "id": "my_isolated_object", + "references": [], + "type": "isolatedtype", + "updated_at": "2017-09-21T18:49:16.270Z", + "version": "WzQ4NywxXQ==" +} + +{ + "attributes": { + "title": "A shared saved-object in all spaces" + }, + "id": "all_spaces", + "references": [], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzQ5NywxXQ==" +} + +{ + "attributes": { + "title": "A shared saved-object in one space" + }, + "id": "default_only", + "references": [ + { + "id": "each_space", + "name": "refname", + "type": "sharedtype" + } + ], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzQ4OCwxXQ==" +} + +{ + "attributes": { + "title": "A shared saved-object in the default, space_1, and space_2 spaces" + }, + "id": "each_space", + "references": [ + { + "id": "default_only", + "name": "refname", + "type": "sharedtype" + }, + { + "id": "space_1_only", + "name": "refname", + "type": "sharedtype" + }, + { + "id": "space_2_only", + "name": "refname", + "type": "sharedtype" + }, + { + "id": "all_spaces", + "name": "refname", + "type": "sharedtype" + } + ], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzQ5NiwxXQ==" +} + +{ + "attributes": { + "title": "This is used to test an inexact match conflict for an id -> originId match" + }, + "id": "conflict_1c_default_and_space_1", + "references": [], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzUwNiwxXQ==" +} + +{ + "attributes": { + "title": "A shared saved-object in the default and space_1 spaces" + }, + "id": "default_and_space_1", + "references": [], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzQ5MywxXQ==" +} + +{ + "attributes": { + "title": "A shared saved-object in the default and space_2 spaces" + }, + "id": "default_and_space_2", + "references": [], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzQ5NCwxXQ==" +} + +{ + "attributes": { + "title": "This object only exists to test the third assertion for spacesWithMatchingOrigins in get_shareable_references" + }, + "id": "space_2_only_matching_origin", + "originId": "space_2_only", + "references": [], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzQ5MiwxXQ==" +} + +{ + "attributes": { + "title": "This is used to test that when an object is unshared from a space, inbound aliases for just those spaces are removed" + }, + "id": "alias_delete_inclusive", + "references": [], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzQ5OCwxXQ==" +} + +{ + "attributes": { + "title": "This is used to test that when an object is unshared from all space, inbound aliases for all spaces are removed" + }, + "id": "alias_delete_exclusive", + "references": [], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzQ5OSwxXQ==" +} + +{ + "attributes": { + "title": "A shared saved-object in the space_1 and space_2 spaces" + }, + "id": "space_1_and_space_2", + "references": [], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzQ5NSwxXQ==" +} diff --git a/x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/space_1.json b/x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/space_1.json new file mode 100644 index 00000000000000..d037b9d1bd24c5 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/space_1.json @@ -0,0 +1,197 @@ +{ + "attributes": { + "title": "Copy to Space index pattern 1 from space_1 space" + }, + "coreMigrationVersion": "8.4.0", + "id": "cts_ip_1_space_1", + "migrationVersion": { + "index-pattern": "8.0.0" + }, + "originId": "cts_ip_1", + "references": [], + "type": "index-pattern", + "updated_at": "2017-09-21T18:49:16.270Z", + "version": "WzUyOSwxXQ==" +} + +{ + "attributes": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "CTS vis 1 from space_1 space", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}" + }, + "coreMigrationVersion": "8.4.0", + "id": "cts_vis_1_space_1", + "migrationVersion": { + "visualization": "8.3.0" + }, + "references": [ + { + "id": "cts_ip_1_space_1", + "name": "CTS IP 1", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2017-09-21T18:49:16.270Z", + "version": "WzUyMSwxXQ==" +} + +{ + "attributes": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "CTS vis 2 from space_1 space", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{},\"legendSize\":\"auto\"},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}" + }, + "coreMigrationVersion": "8.4.0", + "id": "cts_vis_2_space_1", + "migrationVersion": { + "visualization": "8.3.0" + }, + "references": [ + { + "id": "cts_ip_1_space_1", + "name": "CTS IP 1", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2017-09-21T18:49:16.270Z", + "version": "WzUyNiwxXQ==" +} + +{ + "attributes": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "CTS vis 3 from space_1 space", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{},\"legendSize\":\"auto\"},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}" + }, + "coreMigrationVersion": "8.4.0", + "id": "cts_vis_3_space_1", + "migrationVersion": { + "visualization": "8.3.0" + }, + "originId": "cts_vis_3", + "references": [ + { + "id": "cts_ip_1_space_1", + "name": "CTS IP 1", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2017-09-21T18:49:16.270Z", + "version": "WzUyNywxXQ==" +} + +{ + "attributes": { + "description": "Copy to Space Dashboard from space_1 space", + "title": "This is the space_1 test space CTS dashboard" + }, + "coreMigrationVersion": "8.4.0", + "id": "cts_dashboard_space_1", + "migrationVersion": { + "dashboard": "8.4.0" + }, + "originId": "cts_dashboard", + "references": [ + { + "id": "cts_vis_1_space_1", + "name": "CTS Vis 1", + "type": "visualization" + }, + { + "id": "cts_vis_2_space_1", + "name": "CTS Vis 2", + "type": "visualization" + }, + { + "id": "cts_vis_3_space_1", + "name": "CTS Vis 3", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2017-09-21T18:49:16.270Z", + "version": "WzUyNCwxXQ==" +} + +{ + "attributes": { + "title": "A shared saved-object in one space" + }, + "id": "conflict_2_space_1", + "originId": "conflict_2", + "references": [], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzUxMCwxXQ==" +} + +{ + "attributes": { + "title": "This is used to test an inexact match conflict for an originId -> id match" + }, + "id": "conflict_1b_space_1", + "originId": "conflict_1b_space_2", + "references": [], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzUwNCwxXQ==" +} + +{ + "attributes": { + "title": "This is used to test an inexact match conflict for an originId -> originId match" + }, + "id": "conflict_1a_space_1", + "originId": "conflict_1a", + "references": [], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzUwMSwxXQ==" +} + +{ + "attributes": { + "title": "A shared saved-object in one space" + }, + "id": "space_1_only", + "references": [ + { + "id": "each_space", + "name": "refname", + "type": "sharedtype" + } + ], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzQ4OSwxXQ==" +} + +{ + "attributes": { + "title": "Some title" + }, + "id": "my_isolated_object", + "references": [], + "type": "isolatedtype", + "updated_at": "2017-09-21T18:49:16.270Z", + "version": "WzQ4NywxXQ==" +} diff --git a/x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/space_2.json b/x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/space_2.json new file mode 100644 index 00000000000000..c68269eb1b3356 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/space_2.json @@ -0,0 +1,74 @@ +{ + "attributes": { + "title": "A shared saved-object in one space" + }, + "id": "conflict_2_space_2", + "originId": "conflict_2", + "references": [], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzUxMCwxXQ==" +} + +{ + "attributes": { + "title": "This is used to test an inexact match conflict for an id -> originId match" + }, + "id": "conflict_1c_space_2", + "originId": "conflict_1c_default_and_space_1", + "references": [], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzUwNywxXQ==" +} + +{ + "attributes": { + "title": "This is used to test an inexact match conflict for an originId -> id match" + }, + "id": "conflict_1b_space_2", + "references": [], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzUwNSwxXQ==" +} + +{ + "attributes": { + "title": "This is used to test an inexact match conflict for an originId -> originId match" + }, + "id": "conflict_1a_space_2", + "originId": "conflict_1a", + "references": [], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzUwMiwxXQ==" +} + +{ + "attributes": { + "title": "A shared saved-object in one space" + }, + "id": "space_2_only", + "references": [ + { + "id": "each_space", + "name": "refname", + "type": "sharedtype" + } + ], + "type": "sharedtype", + "updated_at": "2017-09-21T18:59:16.270Z", + "version": "WzQ5MSwxXQ==" +} + +{ + "attributes": { + "title": "Some title" + }, + "id": "my_isolated_object", + "references": [], + "type": "isolatedtype", + "updated_at": "2017-09-21T18:49:16.270Z", + "version": "WzQ4NywxXQ==" +} diff --git a/x-pack/test/spaces_api_integration/common/fixtures/spaces_test_plugin/server/plugin.ts b/x-pack/test/spaces_api_integration/common/fixtures/spaces_test_plugin/server/plugin.ts index 1b54ea67554fa3..07cc3bcd28c1b5 100644 --- a/x-pack/test/spaces_api_integration/common/fixtures/spaces_test_plugin/server/plugin.ts +++ b/x-pack/test/spaces_api_integration/common/fixtures/spaces_test_plugin/server/plugin.ts @@ -12,7 +12,7 @@ export class Plugin { public setup(core: CoreSetup) { // called when plugin is setting up during Kibana's startup sequence - core.savedObjects.registerType({ + core.savedObjects.registerType<{ title: string }>({ name: 'sharedtype', hidden: false, namespaceType: 'multiple', @@ -29,7 +29,7 @@ export class Plugin { }, }, }); - core.savedObjects.registerType({ + core.savedObjects.registerType<{ title: string }>({ name: 'isolatedtype', hidden: false, namespaceType: 'single', diff --git a/x-pack/test/spaces_api_integration/common/lib/test_data_loader.ts b/x-pack/test/spaces_api_integration/common/lib/test_data_loader.ts new file mode 100644 index 00000000000000..4b25c722603c87 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/lib/test_data_loader.ts @@ -0,0 +1,128 @@ +/* + * 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 { FtrProviderContext } from '../ftr_provider_context'; + +const SPACE_1 = { + id: 'space_1', + name: 'Space 1', + description: 'This is the first test space', + disabledFeatures: [], +}; + +const SPACE_2 = { + id: 'space_2', + name: 'Space 2', + description: 'This is the second test space', + disabledFeatures: [], +}; + +// Objects can only be imported in one space at a time. To have test saved objects +// that are shared in multiple spaces we should import all objects in the "original" +// spaces first and then share them to other spaces as a subsequent operation. +const OBJECTS_TO_SHARE: Array<{ + spacesToAdd?: string[]; + spacesToRemove?: string[]; + objects: Array<{ type: string; id: string }>; +}> = [ + { + spacesToAdd: ['*'], + spacesToRemove: ['default'], + objects: [ + { type: 'sharedtype', id: 'all_spaces' }, + { type: 'sharedtype', id: 'space_2_only_matching_origin' }, + { type: 'sharedtype', id: 'alias_delete_exclusive' }, + ], + }, + { + spacesToRemove: ['default'], + spacesToAdd: [SPACE_1.id, SPACE_2.id], + objects: [{ type: 'sharedtype', id: 'space_1_and_space_2' }], + }, + { + spacesToAdd: [SPACE_1.id, SPACE_2.id], + objects: [ + { type: 'sharedtype', id: 'each_space' }, + { type: 'sharedtype', id: 'conflict_2_all' }, + { type: 'sharedtype', id: 'alias_delete_inclusive' }, + ], + }, + { + spacesToAdd: [SPACE_1.id], + objects: [ + { type: 'sharedtype', id: 'conflict_1c_default_and_space_1' }, + { type: 'sharedtype', id: 'default_and_space_1' }, + ], + }, + { + spacesToAdd: [SPACE_2.id], + objects: [{ type: 'sharedtype', id: 'default_and_space_2' }], + }, +]; + +export function getTestDataLoader({ getService }: FtrProviderContext) { + const spacesService = getService('spaces'); + const kbnServer = getService('kibanaServer'); + const supertest = getService('supertest'); + const log = getService('log'); + + return { + before: async () => { + await Promise.all([await spacesService.create(SPACE_1), await spacesService.create(SPACE_2)]); + }, + + after: async () => { + await Promise.all([spacesService.delete(SPACE_1.id), spacesService.delete(SPACE_2.id)]); + }, + + beforeEach: async () => { + log.debug('Loading test data for the following spaces: default, space_1 and space_2'); + await Promise.all([ + kbnServer.importExport.load( + 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/default_space.json' + ), + kbnServer.importExport.load( + 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/space_1.json', + { space: SPACE_1.id } + ), + kbnServer.importExport.load( + 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/space_2.json', + { space: SPACE_2.id } + ), + ]); + + // Adjust spaces for the imported saved objects. + for (const { objects, spacesToAdd = [], spacesToRemove = [] } of OBJECTS_TO_SHARE) { + log.debug( + `Updating spaces for the following objects (add: [${spacesToAdd.join( + ', ' + )}], remove: [${spacesToRemove.join(', ')}]): ${objects + .map(({ type, id }) => `${type}:${id}`) + .join(', ')}` + ); + await supertest + .post('/api/spaces/_update_objects_spaces') + .send({ objects, spacesToAdd, spacesToRemove }) + .expect(200); + } + }, + + afterEach: async () => { + const allSpacesIds = [ + ...(await spacesService.getAll()).map((space) => space.id), + 'non_existent_space', + ]; + log.debug(`Removing data from the following spaces: ${allSpacesIds.join(', ')}`); + await Promise.all( + allSpacesIds.flatMap((spaceId) => [ + kbnServer.savedObjects.cleanStandardList({ space: spaceId }), + kbnServer.savedObjects.clean({ space: spaceId, types: ['sharedtype', 'isolatedtype'] }), + ]) + ); + }, + }; +} diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index 72997ead356836..c781eff6d32720 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -6,13 +6,16 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import expect from '@kbn/expect'; -import { SuperTest } from 'supertest'; -import { EsArchiver } from '@kbn/es-archiver'; -import type { Client } from '@elastic/elasticsearch'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; import { CopyResponse } from '@kbn/spaces-plugin/server/lib/copy_to_spaces'; +import { + SavedObjectsImportFailure, + SavedObjectsImportAmbiguousConflictError, +} from '@kbn/core/server'; import { getAggregatedSpaceData, getUrlPrefix } from '../lib/space_test_utils'; import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; +import { getTestDataLoader } from '../lib/test_data_loader'; +import { FtrProviderContext } from '../ftr_provider_context'; type TestResponse = Record<string, any>; @@ -78,11 +81,11 @@ const getDestinationWithConflicts = (originSpaceId?: string) => interface Aggs extends estypes.AggregationsMultiBucketAggregateBase { buckets: SpaceBucket[]; } -export function copyToSpaceTestSuiteFactory( - es: Client, - esArchiver: EsArchiver, - supertest: SuperTest<any> -) { +export function copyToSpaceTestSuiteFactory(context: FtrProviderContext) { + const testDataLoader = getTestDataLoader(context); + const supertestWithoutAuth = context.getService('supertestWithoutAuth'); + const es = context.getService('es'); + const collectSpaceContents = async () => { const response = await getAggregatedSpaceData(es, [ 'visualization', @@ -693,18 +696,22 @@ export function copyToSpaceTestSuiteFactory( if (createNewCopies) { expectNewCopyResponse(response, ambiguousConflictId, title); } else { + // The `updatedAt` values cannot be determined upfront and hence asserted since we update spaces list + // for certain objects in the test setup. + const importAmbiguousConflictError = (errors as SavedObjectsImportFailure[])?.[0] + .error as SavedObjectsImportAmbiguousConflictError; // It doesn't matter if overwrite is enabled or not, the object will not be copied because there are two matches in the destination space const destinations = [ - // response destinations should be sorted by updatedAt in descending order, then ID in ascending order + // response destinations should be sorted by ID in ascending order { id: 'conflict_2_all', title: 'A shared saved-object in all spaces', - updatedAt: '2017-09-21T18:59:16.270Z', + updatedAt: importAmbiguousConflictError?.destinations[0].updatedAt, }, { id: 'conflict_2_space_2', title: 'A shared saved-object in one space', - updatedAt: '2017-09-21T18:59:16.270Z', + updatedAt: importAmbiguousConflictError?.destinations[1].updatedAt, }, ]; expect(success).to.eql(false); @@ -737,22 +744,20 @@ export function copyToSpaceTestSuiteFactory( { user = {}, spaceId = DEFAULT_SPACE_ID, tests }: CopyToSpaceTestDefinition ) => { describeFn(description, () => { - before(() => { + before(async () => { // test data only allows for the following spaces as the copy origin expect(['default', 'space_1']).to.contain(spaceId); + + await testDataLoader.before(); + }); + + after(async () => { + await testDataLoader.after(); }); describe('single-namespace types', () => { - beforeEach(() => - esArchiver.load( - 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces' - ) - ); - afterEach(() => - esArchiver.unload( - 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces' - ) - ); + beforeEach(async () => await testDataLoader.beforeEach()); + afterEach(async () => await testDataLoader.afterEach()); const dashboardObject = { type: 'dashboard', id: `cts_dashboard_${spaceId}` }; @@ -761,7 +766,7 @@ export function copyToSpaceTestSuiteFactory( await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); - return supertest + return supertestWithoutAuth .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) .auth(user.username, user.password) .send({ @@ -780,7 +785,7 @@ export function copyToSpaceTestSuiteFactory( await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); - return supertest + return supertestWithoutAuth .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) .auth(user.username, user.password) .send({ @@ -799,7 +804,7 @@ export function copyToSpaceTestSuiteFactory( await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); - return supertest + return supertestWithoutAuth .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) .auth(user.username, user.password) .send({ @@ -818,7 +823,7 @@ export function copyToSpaceTestSuiteFactory( await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); - return supertest + return supertestWithoutAuth .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) .auth(user.username, user.password) .send({ @@ -836,7 +841,7 @@ export function copyToSpaceTestSuiteFactory( const conflictDestination = getDestinationWithConflicts(spaceId); const noConflictDestination = getDestinationWithoutConflicts(); - return supertest + return supertestWithoutAuth .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) .auth(user.username, user.password) .send({ @@ -869,7 +874,7 @@ export function copyToSpaceTestSuiteFactory( }); it(`should return ${tests.nonExistentSpace.statusCode} when copying to non-existent space`, async () => { - return supertest + return supertestWithoutAuth .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) .auth(user.username, user.password) .send({ @@ -893,21 +898,13 @@ export function copyToSpaceTestSuiteFactory( const spaces = ['space_2']; const includeReferences = false; describe(`multi-namespace types with overwrite=${overwrite} and createNewCopies=${createNewCopies}`, () => { - before(() => - esArchiver.load( - 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces' - ) - ); - after(() => - esArchiver.unload( - 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces' - ) - ); + before(async () => await testDataLoader.beforeEach()); + after(async () => await testDataLoader.afterEach()); const testCases = tests.multiNamespaceTestCases(overwrite, createNewCopies); testCases.forEach(({ testTitle, objects, statusCode, response }) => { it(`should return ${statusCode} when ${testTitle}`, async () => { - return supertest + return supertestWithoutAuth .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) .auth(user.username, user.password) .send({ objects, spaces, includeReferences, createNewCopies, overwrite }) diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts index 81018230f4e07e..2a99ae8afceb61 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts @@ -11,11 +11,7 @@ import { copyToSpaceTestSuiteFactory } from '../../common/suites/copy_to_space'; import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export -export default function copyToSpaceSpacesAndSecuritySuite({ getService }: FtrProviderContext) { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - const es = getService('es'); - +export default function copyToSpaceSpacesAndSecuritySuite(context: FtrProviderContext) { const { copyToSpaceTest, expectNoConflictsWithoutReferencesResult, @@ -27,10 +23,9 @@ export default function copyToSpaceSpacesAndSecuritySuite({ getService }: FtrPro createExpectUnauthorizedAtSpaceWithoutReferencesResult, expectRouteForbiddenResponse, createMultiNamespaceTestCases, - } = copyToSpaceTestSuiteFactory(es, esArchiver, supertestWithoutAuth); + } = copyToSpaceTestSuiteFactory(context); - // Failing: See https://github.com/elastic/kibana/issues/86544 - describe.skip('copy to spaces', () => { + describe('copy to spaces', () => { [ { spaceId: SPACES.DEFAULT.spaceId, diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts index f5422c1f500850..4139e94610f08c 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts @@ -5,15 +5,11 @@ * 2.0. */ -import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; import { copyToSpaceTestSuiteFactory } from '../../common/suites/copy_to_space'; // eslint-disable-next-line import/no-default-export -export default function copyToSpacesOnlySuite({ getService }: FtrProviderContext) { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - const es = getService('es'); - +export default function copyToSpacesOnlySuite(context: FtrProviderContext) { const { copyToSpaceTest, expectNoConflictsWithoutReferencesResult, @@ -23,7 +19,7 @@ export default function copyToSpacesOnlySuite({ getService }: FtrProviderContext createExpectWithConflictsWithoutOverwritingResult, createMultiNamespaceTestCases, originSpaces, - } = copyToSpaceTestSuiteFactory(es, esArchiver, supertestWithoutAuth); + } = copyToSpaceTestSuiteFactory(context); describe('copy to spaces', () => { originSpaces.forEach((spaceId) => { diff --git a/x-pack/test/threat_intelligence_cypress/runner.ts b/x-pack/test/threat_intelligence_cypress/runner.ts index c165cb552b9bac..5ab9d032c55f29 100644 --- a/x-pack/test/threat_intelligence_cypress/runner.ts +++ b/x-pack/test/threat_intelligence_cypress/runner.ts @@ -22,10 +22,7 @@ import { tiAbusechMalwareBazaar } from './pipelines/ti_abusech_malware_bazaar'; import { tiAbusechUrl } from './pipelines/ti_abusech_url'; const retrieveIntegrations = (chunksTotal: number, chunkIndex: number) => { - const pattern = resolve( - __dirname, - '../../plugins/threat_intelligence/cypress/integration/**/*.spec.ts' - ); + const pattern = resolve(__dirname, '../../plugins/threat_intelligence/cypress/e2e/**/*.cy.ts'); const integrationsPaths = globby.sync(pattern); const chunkSize = Math.ceil(integrationsPaths.length / chunksTotal); diff --git a/x-pack/test/visual_regression/services.ts b/x-pack/test/visual_regression/services.ts deleted file mode 100644 index 7d58bd3f35b324..00000000000000 --- a/x-pack/test/visual_regression/services.ts +++ /dev/null @@ -1,14 +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 { services as ossVisualRegressionServices } from '../../../test/visual_regression/services'; -import { services as functionalServices } from '../functional/services'; - -export const services = { - ...functionalServices, - visualTesting: ossVisualRegressionServices.visualTesting, -}; diff --git a/x-pack/test/visual_regression/tests/canvas/fullscreen.js b/x-pack/test/visual_regression/tests/canvas/fullscreen.js deleted file mode 100644 index 6a20db5bccdec4..00000000000000 --- a/x-pack/test/visual_regression/tests/canvas/fullscreen.js +++ /dev/null @@ -1,25 +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. - */ - -export default function ({ getPageObjects, getService }) { - const PageObjects = getPageObjects(['common', 'canvas']); - const visualTesting = getService('visualTesting'); - - describe('fullscreen', () => { - it('workpad should display properly in fullscreen mode', async () => { - await PageObjects.common.navigateToApp('canvas', { - hash: '/workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31/page/1', - }); - - await PageObjects.canvas.enterFullscreen(); - - await PageObjects.canvas.waitForWorkpadElements(); - - await visualTesting.snapshot(); - }); - }); -} diff --git a/x-pack/test/visual_regression/tests/canvas/index.js b/x-pack/test/visual_regression/tests/canvas/index.js deleted file mode 100644 index 20a262fef10fee..00000000000000 --- a/x-pack/test/visual_regression/tests/canvas/index.js +++ /dev/null @@ -1,30 +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 { DEFAULT_OPTIONS } from '../../../../../test/visual_regression/services/visual_testing/visual_testing'; - -const [SCREEN_WIDTH] = DEFAULT_OPTIONS.widths || []; - -export default function ({ loadTestFile, getService }) { - const esArchiver = getService('esArchiver'); - const browser = getService('browser'); - - describe('canvas app visual regression', function () { - before(async () => { - await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); - await esArchiver.load('x-pack/test/functional/es_archives/canvas/default'); - - await browser.setWindowSize(SCREEN_WIDTH, 1000); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/canvas/default'); - }); - - loadTestFile(require.resolve('./fullscreen')); - }); -} diff --git a/x-pack/test/visual_regression/tests/infra/index.js b/x-pack/test/visual_regression/tests/infra/index.js deleted file mode 100644 index 13669c50953f9a..00000000000000 --- a/x-pack/test/visual_regression/tests/infra/index.js +++ /dev/null @@ -1,19 +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. - */ - -export default function ({ loadTestFile, getService }) { - const browser = getService('browser'); - - describe.skip('InfraUI Visual Regression', function () { - before(async () => { - await browser.setWindowSize(1600, 1000); - }); - - loadTestFile(require.resolve('./waffle_map')); - loadTestFile(require.resolve('./saved_views')); - }); -} diff --git a/x-pack/test/visual_regression/tests/infra/saved_views.js b/x-pack/test/visual_regression/tests/infra/saved_views.js deleted file mode 100644 index a2fb3fda206dab..00000000000000 --- a/x-pack/test/visual_regression/tests/infra/saved_views.js +++ /dev/null @@ -1,87 +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 { DATES } from '../../../functional/apps/infra/constants'; -const DATE_WITH_DATA = DATES.metricsAndLogs.hosts.withData; - -export default function ({ getPageObjects, getService }) { - const PageObjects = getPageObjects(['common', 'infraHome', 'infraMetricsExplorer']); - const visualTesting = getService('visualTesting'); - const esArchiver = getService('esArchiver'); - - describe('saved views', () => { - before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs')); - after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs')); - describe('Inverntory Test save functionality', () => { - it('should have save and load controls', async () => { - await PageObjects.common.navigateToApp('infraOps'); - await PageObjects.infraHome.goToTime(DATE_WITH_DATA); - await PageObjects.infraHome.getSaveViewButton(); - await PageObjects.infraHome.getLoadViewsButton(); - await visualTesting.snapshot(); - }); - - it('should open flyout list', async () => { - await PageObjects.infraHome.openSaveViewsFlyout(); - await visualTesting.snapshot(); - await PageObjects.infraHome.closeSavedViewFlyout(); - }); - - it('should open saved view modal', async () => { - await PageObjects.infraHome.openCreateSaveViewModal(); - await visualTesting.snapshot(); - }); - - it('should be able to enter a view name', async () => { - await PageObjects.infraHome.openEnterViewNameAndSave(); - await visualTesting.snapshot(); - }); - - it('should see a saved view in list', async () => { - await PageObjects.infraHome.openSaveViewsFlyout(); - await visualTesting.snapshot(); - }); - }); - - describe('Metric Explorer Test Saved Views', () => { - before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs')); - after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs')); - describe('save functionality', () => { - it('should have saved views component', async () => { - await PageObjects.common.navigateToApp('infraOps'); - await PageObjects.infraHome.goToMetricExplorer(); - await PageObjects.infraSavedViews.getSavedViewsButton(); - await PageObjects.infraSavedViews.ensureViewIsLoaded('Default view'); - await visualTesting.snapshot(); - }); - - it('should open popover', async () => { - await PageObjects.infraSavedViews.clickSavedViewsButton(); - await visualTesting.snapshot(); - await PageObjects.infraSavedViews.closeSavedViewsPopover(); - }); - - it('should create new saved view and load it', async () => { - await PageObjects.infraSavedViews.clickSavedViewsButton(); - await PageObjects.infraSavedViews.clickSaveNewViewButton(); - await PageObjects.infraSavedViews.getCreateSavedViewModal(); - await PageObjects.infraSavedViews.createNewSavedView('view1'); - await PageObjects.infraSavedViews.ensureViewIsLoaded('view1'); - await visualTesting.snapshot(); - }); - - it('should new views should be listed in the load views list', async () => { - await PageObjects.infraSavedViews.clickSavedViewsButton(); - await PageObjects.infraSavedViews.clickLoadViewButton(); - await PageObjects.infraSavedViews.ensureViewIsLoadable('view1'); - await visualTesting.snapshot(); - await PageObjects.infraSavedViews.closeSavedViewsLoadModal(); - }); - }); - }); - }); -} diff --git a/x-pack/test/visual_regression/tests/infra/waffle_map.js b/x-pack/test/visual_regression/tests/infra/waffle_map.js deleted file mode 100644 index 70aaf89a059eb0..00000000000000 --- a/x-pack/test/visual_regression/tests/infra/waffle_map.js +++ /dev/null @@ -1,27 +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 { DATES } from '../../../functional/apps/infra/constants'; -const DATE_WITH_DATA = DATES.metricsAndLogs.hosts.withData; - -export default function ({ getPageObjects, getService }) { - const PageObjects = getPageObjects(['common', 'infraHome']); - const visualTesting = getService('visualTesting'); - const esArchiver = getService('esArchiver'); - - describe('waffle map', () => { - before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs')); - after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs')); - - it('should just work', async () => { - await PageObjects.common.navigateToApp('infraOps'); - await PageObjects.infraHome.goToTime(DATE_WITH_DATA); - await PageObjects.infraHome.getWaffleMap(); - await visualTesting.snapshot(); - }); - }); -} diff --git a/x-pack/test/visual_regression/tests/login_page.ts b/x-pack/test/visual_regression/tests/login_page.ts deleted file mode 100644 index 3e2b4100240361..00000000000000 --- a/x-pack/test/visual_regression/tests/login_page.ts +++ /dev/null @@ -1,58 +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 { FtrProviderContext } from '../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const kibanaServer = getService('kibanaServer'); - const visualTesting = getService('visualTesting'); - const testSubjects = getService('testSubjects'); - const retry = getService('retry'); - const PageObjects = getPageObjects(['common', 'security']); - - describe.skip('Security', () => { - describe('Login Page', () => { - before(async () => { - await kibanaServer.savedObjects.cleanStandardList(); - await PageObjects.security.forceLogout(); - }); - - after(async () => { - await kibanaServer.savedObjects.cleanStandardList(); - }); - - afterEach(async () => { - // NOTE: Logout needs to happen before anything else to avoid flaky behavior - await PageObjects.security.forceLogout(); - }); - - it('renders login page', async () => { - await PageObjects.common.navigateToApp('login'); - - await retry.waitFor( - 'login page visible', - async () => await testSubjects.exists('loginSubmit') - ); - - await visualTesting.snapshot(); - }); - - it('renders failed login', async () => { - await PageObjects.security.loginPage.login('wrong-user', 'wrong-password', { - expectSuccess: false, - }); - - await retry.waitFor( - 'login error visible', - async () => await testSubjects.exists('loginErrorMessage') - ); - - await visualTesting.snapshot(); - }); - }); - }); -} diff --git a/x-pack/test/visual_regression/tests/maps/index.js b/x-pack/test/visual_regression/tests/maps/index.js deleted file mode 100644 index 9d53d70ad2abc4..00000000000000 --- a/x-pack/test/visual_regression/tests/maps/index.js +++ /dev/null @@ -1,61 +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. - */ - -export default function ({ loadTestFile, getService }) { - const kibanaServer = getService('kibanaServer'); - const esArchiver = getService('esArchiver'); - const browser = getService('browser'); - const log = getService('log'); - const supertest = getService('supertest'); - - describe('maps app visual regression', function () { - before(async () => { - await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); - await kibanaServer.importExport.load( - 'x-pack/test/functional/fixtures/kbn_archiver/maps.json' - ); - // Functional tests verify behavior when referenced index pattern saved objects can not be found. - // However, saved object import fails when reference saved objects can not be found. - // To prevent import errors, index pattern saved object references exist during import - // but are then deleted afterwards to enable testing of missing reference index pattern saved objects. - - log.info('Delete index pattern'); - log.debug('id: ' + 'idThatDoesNotExitForESGeoGridSource'); - log.debug('id: ' + 'idThatDoesNotExitForESSearchSource'); - log.debug('id: ' + 'idThatDoesNotExitForESJoinSource'); - await supertest - .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESGeoGridSource') - .set('kbn-xsrf', 'true') - .expect(200); - - await supertest - .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESSearchSource') - .set('kbn-xsrf', 'true') - .expect(200); - - await supertest - .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESJoinSource') - .set('kbn-xsrf', 'true') - .expect(200); - - await esArchiver.load('x-pack/test/functional/es_archives/maps/data'); - await kibanaServer.uiSettings.replace({ - defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', - }); - await browser.setWindowSize(1600, 1000); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/maps/data'); - await kibanaServer.importExport.unload( - 'x-pack/test/functional/fixtures/kbn_archiver/maps.json' - ); - }); - - loadTestFile(require.resolve('./vector_styling')); - }); -} diff --git a/x-pack/test/visual_regression/tests/maps/vector_styling.js b/x-pack/test/visual_regression/tests/maps/vector_styling.js deleted file mode 100644 index 092ae603117d9a..00000000000000 --- a/x-pack/test/visual_regression/tests/maps/vector_styling.js +++ /dev/null @@ -1,49 +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. - */ - -export default function ({ getPageObjects, getService }) { - const PageObjects = getPageObjects(['maps']); - const visualTesting = getService('visualTesting'); - - describe('vector styling', () => { - describe('symbolize as icon', () => { - before(async () => { - await PageObjects.maps.loadSavedMap('vector styling icon demo'); - await PageObjects.maps.enterFullScreen(); - await PageObjects.maps.closeLegend(); - }); - - it('should symbolize points as icons with expected color, size, and orientation', async () => { - await visualTesting.snapshot(); - }); - }); - - describe('dynamic coloring', () => { - before(async () => { - await PageObjects.maps.loadSavedMap('join and dynamic coloring demo'); - await PageObjects.maps.enterFullScreen(); - await PageObjects.maps.closeLegend(); - }); - - it('should symbolize fill color with custom steps from join value and border color with dynamic color ramp from prop value', async () => { - await visualTesting.snapshot(); - }); - }); - - describe('dynamic line coloring', () => { - before(async () => { - await PageObjects.maps.loadSavedMap('pew pew demo'); - await PageObjects.maps.enterFullScreen(); - await PageObjects.maps.closeLegend(); - }); - - it('should symbolize pew pew lines', async () => { - await visualTesting.snapshot(); - }); - }); - }); -} diff --git a/yarn.lock b/yarn.lock index 40141be799b42f..31c3399f9a387b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -81,10 +81,10 @@ dependencies: "@babel/highlight" "^7.18.6" -"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.18.8": - version "7.18.8" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d" - integrity sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ== +"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.18.8", "@babel/compat-data@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.19.0.tgz#2a592fd89bacb1fcde68de31bee4f2f2dacb0e86" + integrity sha512-y5rqgTTPTmaF5e2nVhOxw+Ur9HDJLsWb6U/KpgUzRZEdPfE6VOubXBKLdbcUTijzRptednSBDQbYZBOSqJxpJw== "@babel/core@7.12.9": version "7.12.9" @@ -108,21 +108,21 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@^7.1.0", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.18.13", "@babel/core@^7.7.5": - version "7.18.13" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.13.tgz#9be8c44512751b05094a4d3ab05fc53a47ce00ac" - integrity sha512-ZisbOvRRusFktksHSG6pjj1CSvkPkcZq/KHD45LAkVP/oiHJkNBZWfpvlLmX8OtHDG8IuzsFlVRWo08w7Qxn0A== +"@babel/core@^7.1.0", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.19.0", "@babel/core@^7.7.5": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.19.0.tgz#d2f5f4f2033c00de8096be3c9f45772563e150c3" + integrity sha512-reM4+U7B9ss148rh2n1Qs9ASS+w94irYXga7c2jaQv9RVzpS7Mv1a9rnYYwuDa45G+DkORt9g6An2k/V4d9LbQ== dependencies: "@ampproject/remapping" "^2.1.0" "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.18.13" - "@babel/helper-compilation-targets" "^7.18.9" - "@babel/helper-module-transforms" "^7.18.9" - "@babel/helpers" "^7.18.9" - "@babel/parser" "^7.18.13" + "@babel/generator" "^7.19.0" + "@babel/helper-compilation-targets" "^7.19.0" + "@babel/helper-module-transforms" "^7.19.0" + "@babel/helpers" "^7.19.0" + "@babel/parser" "^7.19.0" "@babel/template" "^7.18.10" - "@babel/traverse" "^7.18.13" - "@babel/types" "^7.18.13" + "@babel/traverse" "^7.19.0" + "@babel/types" "^7.19.0" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -145,12 +145,12 @@ dependencies: eslint-rule-composer "^0.3.0" -"@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.18.13": - version "7.18.13" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.13.tgz#59550cbb9ae79b8def15587bdfbaa388c4abf212" - integrity sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ== +"@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.19.0.tgz#785596c06425e59334df2ccee63ab166b738419a" + integrity sha512-S1ahxf1gZ2dpoiFgA+ohK9DIpz50bJ0CWs7Zlzb54Z4sG8qmdIrGrVqmy1sAtTVRb+9CU6U8VqT9L0Zj7hxHVg== dependencies: - "@babel/types" "^7.18.13" + "@babel/types" "^7.19.0" "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" @@ -169,12 +169,12 @@ "@babel/helper-explode-assignable-expression" "^7.18.6" "@babel/types" "^7.18.9" -"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz#69e64f57b524cde3e5ff6cc5a9f4a387ee5563bf" - integrity sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg== +"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.0.tgz#537ec8339d53e806ed422f1e06c8f17d55b96bb0" + integrity sha512-Ai5bNWXIvwDvWM7njqsG3feMlL9hCVQsPYXodsZyLwshYkZVJt59Gftau4VrE8S9IT9asd2uSP1hG6wCNw+sXA== dependencies: - "@babel/compat-data" "^7.18.8" + "@babel/compat-data" "^7.19.0" "@babel/helper-validator-option" "^7.18.6" browserslist "^4.20.2" semver "^6.3.0" @@ -192,10 +192,10 @@ "@babel/helper-replace-supers" "^7.18.9" "@babel/helper-split-export-declaration" "^7.18.6" -"@babel/helper-create-regexp-features-plugin@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.18.6.tgz#3e35f4e04acbbf25f1b3534a657610a000543d3c" - integrity sha512-7LcpH1wnQLGrI+4v+nPp+zUvIkF9x0ddv1Hkdue10tg3gmRnLy97DXh4STiOf1qeIInyD69Qv5kKSZzKD8B/7A== +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.19.0.tgz#7976aca61c0984202baca73d84e2337a5424a41b" + integrity sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" regexpu-core "^5.1.0" @@ -238,13 +238,13 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-function-name@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz#940e6084a55dee867d33b4e487da2676365e86b0" - integrity sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A== +"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c" + integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w== dependencies: - "@babel/template" "^7.18.6" - "@babel/types" "^7.18.9" + "@babel/template" "^7.18.10" + "@babel/types" "^7.19.0" "@babel/helper-hoist-variables@^7.18.6": version "7.18.6" @@ -267,19 +267,19 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz#5a1079c005135ed627442df31a42887e80fcb712" - integrity sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g== +"@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz#309b230f04e22c58c6a2c0c0c7e50b216d350c30" + integrity sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ== dependencies: "@babel/helper-environment-visitor" "^7.18.9" "@babel/helper-module-imports" "^7.18.6" "@babel/helper-simple-access" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6" "@babel/helper-validator-identifier" "^7.18.6" - "@babel/template" "^7.18.6" - "@babel/traverse" "^7.18.9" - "@babel/types" "^7.18.9" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.19.0" + "@babel/types" "^7.19.0" "@babel/helper-optimise-call-expression@^7.18.6": version "7.18.6" @@ -293,10 +293,10 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz#4b8aea3b069d8cb8a72cdfe28ddf5ceca695ef2f" - integrity sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w== +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz#4796bb14961521f0f8715990bee2fb6e51ce21bf" + integrity sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw== "@babel/helper-remap-async-to-generator@^7.18.6", "@babel/helper-remap-async-to-generator@^7.18.9": version "7.18.9" @@ -365,14 +365,14 @@ "@babel/traverse" "^7.18.9" "@babel/types" "^7.18.9" -"@babel/helpers@^7.12.5", "@babel/helpers@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.9.tgz#4bef3b893f253a1eced04516824ede94dcfe7ff9" - integrity sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ== +"@babel/helpers@^7.12.5", "@babel/helpers@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.19.0.tgz#f30534657faf246ae96551d88dd31e9d1fa1fc18" + integrity sha512-DRBCKGwIEdqY3+rPJgG/dKfQy9+08rHIAJx8q2p+HSWP87s2HCrQmaAMMyMll2kIXKCW0cO1RdQskx15Xakftg== dependencies: - "@babel/template" "^7.18.6" - "@babel/traverse" "^7.18.9" - "@babel/types" "^7.18.9" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.19.0" + "@babel/types" "^7.19.0" "@babel/highlight@^7.10.4", "@babel/highlight@^7.18.6": version "7.18.6" @@ -383,10 +383,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.10.3", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.18.13": - version "7.18.13" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.13.tgz#5b2dd21cae4a2c5145f1fbd8ca103f9313d3b7e4" - integrity sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg== +"@babel/parser@^7.1.0", "@babel/parser@^7.10.3", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.0.tgz#497fcafb1d5b61376959c1c338745ef0577aa02c" + integrity sha512-74bEXKX2h+8rrfQUfsBfuZZHzsEs6Eql4pqy/T4Nn6Y9wNPggQOqD6z6pn5Bl8ZfysKouFZT/UXEH94ummEeQw== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" @@ -404,17 +404,17 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" "@babel/plugin-proposal-optional-chaining" "^7.18.9" -"@babel/plugin-proposal-async-generator-functions@^7.18.10": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.18.10.tgz#85ea478c98b0095c3e4102bff3b67d306ed24952" - integrity sha512-1mFuY2TOsR1hxbjCo4QL+qlIjV07p4H4EUYw2J/WCqsvFV6V9X9z9YhXbWndc/4fw+hYGlDT7egYxliMp5O6Ew== +"@babel/plugin-proposal-async-generator-functions@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.19.0.tgz#cf5740194f170467df20581712400487efc79ff1" + integrity sha512-nhEByMUTx3uZueJ/QkJuSlCfN4FGg+xy+vRsfGQGzSauq5ks2Deid2+05Q3KhfaUjvec1IGhw/Zm3cFm8JigTQ== dependencies: "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-plugin-utils" "^7.19.0" "@babel/helper-remap-async-to-generator" "^7.18.9" "@babel/plugin-syntax-async-generators" "^7.8.4" -"@babel/plugin-proposal-class-properties@^7.12.1", "@babel/plugin-proposal-class-properties@^7.16.0", "@babel/plugin-proposal-class-properties@^7.18.6": +"@babel/plugin-proposal-class-properties@^7.12.1", "@babel/plugin-proposal-class-properties@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== @@ -505,7 +505,7 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.0" "@babel/plugin-transform-parameters" "^7.12.1" -"@babel/plugin-proposal-object-rest-spread@^7.12.1", "@babel/plugin-proposal-object-rest-spread@^7.16.0", "@babel/plugin-proposal-object-rest-spread@^7.18.9": +"@babel/plugin-proposal-object-rest-spread@^7.12.1", "@babel/plugin-proposal-object-rest-spread@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.18.9.tgz#f9434f6beb2c8cae9dfcf97d2a5941bbbf9ad4e7" integrity sha512-kDDHQ5rflIeY5xl69CEqGEZ0KY369ehsCIEbTGb4siHG5BE9sga/T0r0OUwyZNLMmZE79E1kbsqAjwFCW4ds6Q== @@ -750,16 +750,17 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.9" -"@babel/plugin-transform-classes@^7.12.1", "@babel/plugin-transform-classes@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.18.9.tgz#90818efc5b9746879b869d5ce83eb2aa48bbc3da" - integrity sha512-EkRQxsxoytpTlKJmSPYrsOMjCILacAjtSVkd4gChEe2kXjFCun3yohhW5I7plXJhCemM0gKsaGMcO8tinvCA5g== +"@babel/plugin-transform-classes@^7.12.1", "@babel/plugin-transform-classes@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.19.0.tgz#0e61ec257fba409c41372175e7c1e606dc79bb20" + integrity sha512-YfeEE9kCjqTS9IitkgfJuxjcEtLUHMqa8yUJ6zdz8vR7hKuo6mOy2C05P0F1tdMmDCeuyidKnlrw/iTppHcr2A== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-compilation-targets" "^7.19.0" "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.18.9" + "@babel/helper-function-name" "^7.19.0" "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-plugin-utils" "^7.19.0" "@babel/helper-replace-supers" "^7.18.9" "@babel/helper-split-export-declaration" "^7.18.6" globals "^11.1.0" @@ -771,10 +772,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.9" -"@babel/plugin-transform-destructuring@^7.12.1", "@babel/plugin-transform-destructuring@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.9.tgz#68906549c021cb231bee1db21d3b5b095f8ee292" - integrity sha512-p5VCYNddPLkZTq4XymQIaIfZNJwT9YsjkPOhkVEqt6QIpQFZVM9IltqqYpOEkJoN1DPznmxUDyZ5CTZs/ZCuHA== +"@babel/plugin-transform-destructuring@^7.12.1", "@babel/plugin-transform-destructuring@^7.18.13": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.13.tgz#9e03bc4a94475d62b7f4114938e6c5c33372cbf5" + integrity sha512-TodpQ29XekIsex2A+YJPj5ax2plkGa8YYY6mFjCohk/IG9IY42Rtuj1FuDeemfg2ipxIFLzPeA83SIBnlhSIow== dependencies: "@babel/helper-plugin-utils" "^7.18.9" @@ -858,14 +859,14 @@ "@babel/helper-simple-access" "^7.18.6" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-systemjs@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.18.9.tgz#545df284a7ac6a05125e3e405e536c5853099a06" - integrity sha512-zY/VSIbbqtoRoJKo2cDTewL364jSlZGvn0LKOf9ntbfxOvjfmyrdtEEOAdswOswhZEb8UH3jDkCKHd1sPgsS0A== +"@babel/plugin-transform-modules-systemjs@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.0.tgz#5f20b471284430f02d9c5059d9b9a16d4b085a1f" + integrity sha512-x9aiR0WXAWmOWsqcsnrzGR+ieaTMVyGyffPVA7F8cXAGt/UxefYv6uSHZLkAFChN5M5Iy1+wjE+xJuPt22H39A== dependencies: "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-module-transforms" "^7.18.9" - "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-module-transforms" "^7.19.0" + "@babel/helper-plugin-utils" "^7.19.0" "@babel/helper-validator-identifier" "^7.18.6" babel-plugin-dynamic-import-node "^2.3.3" @@ -877,13 +878,13 @@ "@babel/helper-module-transforms" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-named-capturing-groups-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.18.6.tgz#c89bfbc7cc6805d692f3a49bc5fc1b630007246d" - integrity sha512-UmEOGF8XgaIqD74bC8g7iV3RYj8lMf0Bw7NJzvnS9qQhM4mg+1WHKotUIdjxgD2RGrgFLZZPCFPFj3P/kVDYhg== +"@babel/plugin-transform-named-capturing-groups-regex@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.19.0.tgz#58c52422e4f91a381727faed7d513c89d7f41ada" + integrity sha512-HDSuqOQzkU//kfGdiHBt71/hkDTApw4U/cMVgKgX7PqfB3LOaK+2GtCEsBu1dL9CkswDm0Gwehht1dCr421ULQ== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-create-regexp-features-plugin" "^7.19.0" + "@babel/helper-plugin-utils" "^7.19.0" "@babel/plugin-transform-new-target@^7.18.6": version "7.18.6" @@ -962,7 +963,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-runtime@^7.16.0", "@babel/plugin-transform-runtime@^7.18.10": +"@babel/plugin-transform-runtime@^7.18.10": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.18.10.tgz#37d14d1fa810a368fd635d4d1476c0154144a96f" integrity sha512-q5mMeYAdfEbpBAgzl7tBre/la3LeCxmDO1+wMXRdPWbcoMjR3GiXlCLk7JBZVVye0bqTGNMbt0yYVXX1B1jEWQ== @@ -981,12 +982,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-spread@^7.12.1", "@babel/plugin-transform-spread@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.18.9.tgz#6ea7a6297740f381c540ac56caf75b05b74fb664" - integrity sha512-39Q814wyoOPtIB/qGopNIL9xDChOE1pNU0ZY5dO0owhiVt/5kFm4li+/bBtwc7QotG0u5EPzqhZdjMtmqBqyQA== +"@babel/plugin-transform-spread@^7.12.1", "@babel/plugin-transform-spread@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.19.0.tgz#dd60b4620c2fec806d60cfaae364ec2188d593b6" + integrity sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w== dependencies: - "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-plugin-utils" "^7.19.0" "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" "@babel/plugin-transform-sticky-regex@^7.18.6": @@ -1034,18 +1035,18 @@ "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/preset-env@^7.12.11", "@babel/preset-env@^7.16.0", "@babel/preset-env@^7.18.10": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.18.10.tgz#83b8dfe70d7eea1aae5a10635ab0a5fe60dfc0f4" - integrity sha512-wVxs1yjFdW3Z/XkNfXKoblxoHgbtUF7/l3PvvP4m02Qz9TZ6uZGxRVYjSQeR87oQmHco9zWitW5J82DJ7sCjvA== +"@babel/preset-env@^7.12.11", "@babel/preset-env@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.19.0.tgz#fd18caf499a67d6411b9ded68dc70d01ed1e5da7" + integrity sha512-1YUju1TAFuzjIQqNM9WsF4U6VbD/8t3wEAlw3LFYuuEr+ywqLRcSXxFKz4DCEj+sN94l/XTDiUXYRrsvMpz9WQ== dependencies: - "@babel/compat-data" "^7.18.8" - "@babel/helper-compilation-targets" "^7.18.9" - "@babel/helper-plugin-utils" "^7.18.9" + "@babel/compat-data" "^7.19.0" + "@babel/helper-compilation-targets" "^7.19.0" + "@babel/helper-plugin-utils" "^7.19.0" "@babel/helper-validator-option" "^7.18.6" "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6" "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.18.9" - "@babel/plugin-proposal-async-generator-functions" "^7.18.10" + "@babel/plugin-proposal-async-generator-functions" "^7.19.0" "@babel/plugin-proposal-class-properties" "^7.18.6" "@babel/plugin-proposal-class-static-block" "^7.18.6" "@babel/plugin-proposal-dynamic-import" "^7.18.6" @@ -1079,9 +1080,9 @@ "@babel/plugin-transform-async-to-generator" "^7.18.6" "@babel/plugin-transform-block-scoped-functions" "^7.18.6" "@babel/plugin-transform-block-scoping" "^7.18.9" - "@babel/plugin-transform-classes" "^7.18.9" + "@babel/plugin-transform-classes" "^7.19.0" "@babel/plugin-transform-computed-properties" "^7.18.9" - "@babel/plugin-transform-destructuring" "^7.18.9" + "@babel/plugin-transform-destructuring" "^7.18.13" "@babel/plugin-transform-dotall-regex" "^7.18.6" "@babel/plugin-transform-duplicate-keys" "^7.18.9" "@babel/plugin-transform-exponentiation-operator" "^7.18.6" @@ -1091,9 +1092,9 @@ "@babel/plugin-transform-member-expression-literals" "^7.18.6" "@babel/plugin-transform-modules-amd" "^7.18.6" "@babel/plugin-transform-modules-commonjs" "^7.18.6" - "@babel/plugin-transform-modules-systemjs" "^7.18.9" + "@babel/plugin-transform-modules-systemjs" "^7.19.0" "@babel/plugin-transform-modules-umd" "^7.18.6" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.18.6" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.19.0" "@babel/plugin-transform-new-target" "^7.18.6" "@babel/plugin-transform-object-super" "^7.18.6" "@babel/plugin-transform-parameters" "^7.18.8" @@ -1101,14 +1102,14 @@ "@babel/plugin-transform-regenerator" "^7.18.6" "@babel/plugin-transform-reserved-words" "^7.18.6" "@babel/plugin-transform-shorthand-properties" "^7.18.6" - "@babel/plugin-transform-spread" "^7.18.9" + "@babel/plugin-transform-spread" "^7.19.0" "@babel/plugin-transform-sticky-regex" "^7.18.6" "@babel/plugin-transform-template-literals" "^7.18.9" "@babel/plugin-transform-typeof-symbol" "^7.18.9" "@babel/plugin-transform-unicode-escapes" "^7.18.10" "@babel/plugin-transform-unicode-regex" "^7.18.6" "@babel/preset-modules" "^0.1.5" - "@babel/types" "^7.18.10" + "@babel/types" "^7.19.0" babel-plugin-polyfill-corejs2 "^0.3.2" babel-plugin-polyfill-corejs3 "^0.5.3" babel-plugin-polyfill-regenerator "^0.4.0" @@ -1134,7 +1135,7 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/preset-react@^7.12.10", "@babel/preset-react@^7.16.0", "@babel/preset-react@^7.18.6": +"@babel/preset-react@^7.12.10", "@babel/preset-react@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.18.6.tgz#979f76d6277048dc19094c217b507f3ad517dd2d" integrity sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg== @@ -1174,10 +1175,10 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.0", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.0", "@babel/runtime@^7.18.9", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" - integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.0", "@babel/runtime@^7.15.4", "@babel/runtime@^7.19.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259" + integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA== dependencies: regenerator-runtime "^0.13.4" @@ -1190,26 +1191,26 @@ "@babel/parser" "^7.18.10" "@babel/types" "^7.18.10" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.10.3", "@babel/traverse@^7.12.11", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.18.13", "@babel/traverse@^7.18.9", "@babel/traverse@^7.4.5": - version "7.18.13" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.13.tgz#5ab59ef51a997b3f10c4587d648b9696b6cb1a68" - integrity sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA== +"@babel/traverse@^7.1.0", "@babel/traverse@^7.10.3", "@babel/traverse@^7.12.11", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.18.9", "@babel/traverse@^7.19.0", "@babel/traverse@^7.4.5": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.19.0.tgz#eb9c561c7360005c592cc645abafe0c3c4548eed" + integrity sha512-4pKpFRDh+utd2mbRC8JLnlsMUii3PMHjpL6a0SZ4NMZy7YFP9aXORxEhdMVOc9CpWtDF09IkciQLEhK7Ml7gRA== dependencies: "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.18.13" + "@babel/generator" "^7.19.0" "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.18.9" + "@babel/helper-function-name" "^7.19.0" "@babel/helper-hoist-variables" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.18.13" - "@babel/types" "^7.18.13" + "@babel/parser" "^7.19.0" + "@babel/types" "^7.19.0" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.10.3", "@babel/types@^7.12.11", "@babel/types@^7.12.7", "@babel/types@^7.18.10", "@babel/types@^7.18.13", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": - version "7.18.13" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.13.tgz#30aeb9e514f4100f7c1cb6e5ba472b30e48f519a" - integrity sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ== +"@babel/types@^7.0.0", "@babel/types@^7.10.3", "@babel/types@^7.12.11", "@babel/types@^7.12.7", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.19.0.tgz#75f21d73d73dc0351f3368d28db73465f4814600" + integrity sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA== dependencies: "@babel/helper-string-parser" "^7.18.10" "@babel/helper-validator-identifier" "^7.18.6" @@ -1308,39 +1309,15 @@ resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.1.tgz#b6b8d81780b9a9f6459f4bfe9226ac6aefaefe87" integrity sha512-aG20vknL4/YjQF9BSV7ts4EWm/yrjagAN7OWBNmlbEOUiu0llj4OGrFoOKK3g2vey4/p2omKCoHrWtPxSwV3HA== -"@cypress/browserify-preprocessor@3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@cypress/browserify-preprocessor/-/browserify-preprocessor-3.0.2.tgz#1dbecae394937aed47a3524cad47086c2ded8c50" - integrity sha512-y6mlFR+IR2cqcm3HabSp7AEcX9QfF1EUL4eOaw/7xexdhmdQU8ez6piyRopZQob4BK8oKTsc9PkupsU2rzjqMA== - dependencies: - "@babel/core" "^7.16.0" - "@babel/plugin-proposal-class-properties" "^7.16.0" - "@babel/plugin-proposal-object-rest-spread" "^7.16.0" - "@babel/plugin-transform-runtime" "^7.16.0" - "@babel/preset-env" "^7.16.0" - "@babel/preset-react" "^7.16.0" - "@babel/runtime" "^7.16.0" - babel-plugin-add-module-exports "^1.0.4" - babelify "^10.0.0" - bluebird "^3.7.2" - browserify "^16.2.3" - coffeeify "^3.0.1" - coffeescript "^1.12.7" - debug "^4.3.2" - fs-extra "^9.0.0" - lodash.clonedeep "^4.5.0" - through2 "^2.0.0" - watchify "^4.0.0" - -"@cypress/code-coverage@^3.9.12": - version "3.9.12" - resolved "https://registry.yarnpkg.com/@cypress/code-coverage/-/code-coverage-3.9.12.tgz#f1eab362a71734f997dfb870342cecff20dae23d" - integrity sha512-2QuDSQ2ovz2ZsbQImM917q+9JmEq4afC4kpgHe2o3rTQxUrs7CdHM84rT8XKl0gJIXmbMcNq2rZqe40/eFmCFw== +"@cypress/code-coverage@^3.10.0": + version "3.10.0" + resolved "https://registry.yarnpkg.com/@cypress/code-coverage/-/code-coverage-3.10.0.tgz#2132dbb7ae068cab91790926d50a9bf85140cab4" + integrity sha512-K5pW2KPpK4vKMXqxd6vuzo6m9BNgpAv1LcrrtmqAtOJ1RGoEILXYZVost0L6Q+V01NyY7n7jXIIfS7LR3nP6YA== dependencies: - "@cypress/browserify-preprocessor" "3.0.2" + "@cypress/webpack-preprocessor" "^5.11.0" chalk "4.1.2" dayjs "1.10.7" - debug "4.3.3" + debug "4.3.4" execa "4.1.0" globby "11.0.4" istanbul-lib-coverage "3.0.0" @@ -1385,13 +1362,13 @@ snap-shot-compare "2.8.3" snap-shot-store "1.2.3" -"@cypress/webpack-preprocessor@^5.6.0": - version "5.6.0" - resolved "https://registry.yarnpkg.com/@cypress/webpack-preprocessor/-/webpack-preprocessor-5.6.0.tgz#9648ae22d2e52f17a604e2a493af27a9c96568bd" - integrity sha512-kSelTDe6gs3Skp4vPP2vfTvAl+Ua+9rR/AMTir7bgJihDvzFESqnjWtF6N1TrPo+vCFVGx0VUA6JUvDkhvpwhA== +"@cypress/webpack-preprocessor@^5.11.0", "@cypress/webpack-preprocessor@^5.12.2": + version "5.12.2" + resolved "https://registry.yarnpkg.com/@cypress/webpack-preprocessor/-/webpack-preprocessor-5.12.2.tgz#9cc623a5629980d7f2619569bffc8e3f05a701ae" + integrity sha512-t29wEFvI87IMnCd8taRunwStNsFjFWg138fGF0hPQOYgSj30fbzCEwFD9cAQLYMMcjjuXcnnw8yOfkzIZBBNVQ== dependencies: - bluebird "^3.7.1" - debug "4.3.2" + bluebird "3.7.1" + debug "^4.3.2" lodash "^4.17.20" "@cypress/xvfb@^1.2.4": @@ -2767,6 +2744,14 @@ version "0.0.0" uid "" +"@kbn/core-apps-browser-internal@link:bazel-bin/packages/core/apps/core-apps-browser-internal": + version "0.0.0" + uid "" + +"@kbn/core-apps-browser-mocks@link:bazel-bin/packages/core/apps/core-apps-browser-mocks": + version "0.0.0" + uid "" + "@kbn/core-base-browser-internal@link:bazel-bin/packages/core/base/core-base-browser-internal": version "0.0.0" uid "" @@ -3703,7 +3688,11 @@ version "0.0.0" uid "" -"@kbn/shared-ux-storybook-config@link:bazel-bin/packages/shared-ux/storybook/config": +"@kbn/shared-ux-router-mocks@link:bazel-bin/packages/shared-ux/router/mocks": + version "0.0.0" + uid "" + +"@kbn/shared-ux-services@link:bazel-bin/packages/kbn-shared-ux-services": version "0.0.0" uid "" @@ -3711,6 +3700,10 @@ version "0.0.0" uid "" +"@kbn/shared-ux-storybook@link:bazel-bin/packages/kbn-shared-ux-storybook": + version "0.0.0" + uid "" + "@kbn/shared-ux-utility@link:bazel-bin/packages/kbn-shared-ux-utility": version "0.0.0" uid "" @@ -4164,90 +4157,6 @@ dependencies: mkdirp "^1.0.4" -"@oclif/color@^0.0.0": - version "0.0.0" - resolved "https://registry.yarnpkg.com/@oclif/color/-/color-0.0.0.tgz#54939bbd16d1387511bf1a48ccda1a417248e6a9" - integrity sha512-KKd3W7eNwfNF061tr663oUNdt8EMnfuyf5Xv55SGWA1a0rjhWqS/32P7OeB7CbXcJUBdfVrPyR//1afaW12AWw== - dependencies: - ansi-styles "^3.2.1" - supports-color "^5.4.0" - tslib "^1" - -"@oclif/command@1.5.19", "@oclif/command@^1.5.13", "@oclif/command@^1.5.3": - version "1.5.19" - resolved "https://registry.yarnpkg.com/@oclif/command/-/command-1.5.19.tgz#13f472450eb83bd6c6871a164c03eadb5e1a07ed" - integrity sha512-6+iaCMh/JXJaB2QWikqvGE9//wLEVYYwZd5sud8aLoLKog1Q75naZh2vlGVtg5Mq/NqpqGQvdIjJb3Bm+64AUQ== - dependencies: - "@oclif/config" "^1" - "@oclif/errors" "^1.2.2" - "@oclif/parser" "^3.8.3" - "@oclif/plugin-help" "^2" - debug "^4.1.1" - semver "^5.6.0" - -"@oclif/config@^1": - version "1.13.0" - resolved "https://registry.yarnpkg.com/@oclif/config/-/config-1.13.0.tgz#fc2bd82a9cb30a73faf7d2aa5ae937c719492bd1" - integrity sha512-ttb4l85q7SBx+WlUJY4A9eXLgv4i7hGDNGaXnY9fDKrYD7PBMwNOQ3Ssn2YT2yARAjyOxVE/5LfcwhQGq4kzqg== - dependencies: - debug "^4.1.1" - tslib "^1.9.3" - -"@oclif/errors@^1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@oclif/errors/-/errors-1.2.2.tgz#9d8f269b15f13d70aa93316fed7bebc24688edc2" - integrity sha512-Eq8BFuJUQcbAPVofDxwdE0bL14inIiwt5EaKRVY9ZDIG11jwdXZqiQEECJx0VfnLyUZdYfRd/znDI/MytdJoKg== - dependencies: - clean-stack "^1.3.0" - fs-extra "^7.0.0" - indent-string "^3.2.0" - strip-ansi "^5.0.0" - wrap-ansi "^4.0.0" - -"@oclif/linewrap@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@oclif/linewrap/-/linewrap-1.0.0.tgz#aedcb64b479d4db7be24196384897b5000901d91" - integrity sha512-Ups2dShK52xXa8w6iBWLgcjPJWjais6KPJQq3gQ/88AY6BXoTX+MIGFPrWQO1KLMiQfoTpcLnUwloN4brrVUHw== - -"@oclif/parser@^3.8.3": - version "3.8.4" - resolved "https://registry.yarnpkg.com/@oclif/parser/-/parser-3.8.4.tgz#1a90fc770a42792e574fb896325618aebbe8c9e4" - integrity sha512-cyP1at3l42kQHZtqDS3KfTeyMvxITGwXwH1qk9ktBYvqgMp5h4vHT+cOD74ld3RqJUOZY/+Zi9lb4Tbza3BtuA== - dependencies: - "@oclif/linewrap" "^1.0.0" - chalk "^2.4.2" - tslib "^1.9.3" - -"@oclif/plugin-help@^2": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@oclif/plugin-help/-/plugin-help-2.2.0.tgz#8dfc1c80deae47a205fbc70b018747ba93f31cc3" - integrity sha512-56iIgE7NQfwy/ZrWrvrEfJGb5rrMUt409yoQGw4feiU101UudA1btN1pbUbcKBr7vY9KFeqZZcftXEGxOp7zBg== - dependencies: - "@oclif/command" "^1.5.13" - chalk "^2.4.1" - indent-string "^3.2.0" - lodash.template "^4.4.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" - widest-line "^2.0.1" - wrap-ansi "^4.0.0" - -"@oclif/plugin-not-found@^1.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@oclif/plugin-not-found/-/plugin-not-found-1.2.2.tgz#3e601f6e4264d7a0268cd03c152d90aa9c0cec6d" - integrity sha512-SPlmiJFmTFltQT/owdzQwKgq6eq5AEKVwVK31JqbzK48bRWvEL1Ye60cgztXyZ4bpPn2Fl+KeL3FWFQX41qJuA== - dependencies: - "@oclif/color" "^0.0.0" - "@oclif/command" "^1.5.3" - cli-ux "^4.9.0" - fast-levenshtein "^2.0.6" - lodash "^4.17.11" - -"@oclif/screen@^1.0.3": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@oclif/screen/-/screen-1.0.4.tgz#b740f68609dfae8aa71c3a6cab15d816407ba493" - integrity sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw== - "@octokit/app@^2.2.2": version "2.2.2" resolved "https://registry.yarnpkg.com/@octokit/app/-/app-2.2.2.tgz#a1b8248f64159eeccbe4000d888fdae4163c4ad8" @@ -4607,34 +4516,6 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.4.0.tgz#facf2c67d6063b9918d5a5e3fdf25f3a30d547b6" integrity sha512-Hzl8soGpmyzja9w3kiFFcYJ7n5HNETpplY6cb67KR4QPlxp4FTTresO06qXHgHDhyIInmbLJXuwARjjpsKYGuQ== -"@percy/agent@^0.28.6": - version "0.28.6" - resolved "https://registry.yarnpkg.com/@percy/agent/-/agent-0.28.6.tgz#b220fab6ddcf63ae4e6c343108ba6955a772ce1c" - integrity sha512-SDAyBiUmfQMVTayjvEjQ0IJIA7Y3AoeyWn0jmUxNOMRRIJWo4lQJghfhFCgzCkhXDCm67NMN2nAQAsvXrlIdkQ== - dependencies: - "@oclif/command" "1.5.19" - "@oclif/config" "^1" - "@oclif/plugin-help" "^2" - "@oclif/plugin-not-found" "^1.2" - axios "^0.21.1" - body-parser "^1.18.3" - colors "^1.3.2" - cors "^2.8.4" - cosmiconfig "^5.2.1" - cross-spawn "^7.0.2" - deepmerge "^4.0.0" - express "^4.16.3" - follow-redirects "1.12.1" - generic-pool "^3.7.1" - globby "^10.0.1" - image-size "^0.8.2" - js-yaml "^3.13.1" - percy-client "^3.2.0" - puppeteer "^5.3.1" - retry-axios "^1.0.1" - which "^2.0.1" - winston "^3.0.0" - "@pmmmwh/react-refresh-webpack-plugin@^0.5.1": version "0.5.5" resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.5.tgz#e77aac783bd079f548daa0a7f080ab5b5a9741ca" @@ -6085,14 +5966,7 @@ "@types/babel__template" "*" "@types/babel__traverse" "*" -"@types/babel__generator@*": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.0.2.tgz#d2112a6b21fad600d7674274293c85dce0cb47fc" - integrity sha512-NHcOfab3Zw4q5sEE2COkpfXjoE7o+PmqD9DQW4koUT3roNxwziUdXGnRndMat/LJNUtePwn1TlP4do3uoe3KZQ== - dependencies: - "@babel/types" "^7.0.0" - -"@types/babel__generator@^7.6.4": +"@types/babel__generator@*", "@types/babel__generator@^7.6.4": version "7.6.4" resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== @@ -6861,6 +6735,14 @@ version "0.0.0" uid "" +"@types/kbn__core-apps-browser-internal@link:bazel-bin/packages/core/apps/core-apps-browser-internal/npm_module_types": + version "0.0.0" + uid "" + +"@types/kbn__core-apps-browser-mocks@link:bazel-bin/packages/core/apps/core-apps-browser-mocks/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__core-base-browser-internal@link:bazel-bin/packages/core/base/core-base-browser-internal/npm_module_types": version "0.0.0" uid "" @@ -7797,7 +7679,11 @@ version "0.0.0" uid "" -"@types/kbn__shared-ux-storybook-config@link:bazel-bin/packages/shared-ux/storybook/config/npm_module_types": +"@types/kbn__shared-ux-router-mocks@link:bazel-bin/packages/shared-ux/router/mocks/npm_module_types": + version "0.0.0" + uid "" + +"@types/kbn__shared-ux-services@link:bazel-bin/packages/kbn-shared-ux-services/npm_module_types": version "0.0.0" uid "" @@ -7805,6 +7691,10 @@ version "0.0.0" uid "" +"@types/kbn__shared-ux-storybook@link:bazel-bin/packages/kbn-shared-ux-storybook/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__shared-ux-utility@link:bazel-bin/packages/kbn-shared-ux-utility/npm_module_types": version "0.0.0" uid "" @@ -9199,7 +9089,7 @@ resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== -JSONStream@1.3.5, JSONStream@^1.0.3: +JSONStream@1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== @@ -9245,7 +9135,7 @@ acorn-jsx@^5.3.1: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== -acorn-node@^1.2.0, acorn-node@^1.3.0, acorn-node@^1.5.2, acorn-node@^1.6.1: +acorn-node@^1.3.0, acorn-node@^1.6.1: version "1.8.2" resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8" integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A== @@ -9306,11 +9196,6 @@ agent-base@4: dependencies: es6-promisify "^5.0.0" -agent-base@5: - version "5.1.1" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" - integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g== - agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -9443,7 +9328,7 @@ ansi-colors@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== -ansi-escapes@^3.0.0, ansi-escapes@^3.1.0: +ansi-escapes@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== @@ -9530,11 +9415,6 @@ ansi-wrap@0.1.0, ansi-wrap@^0.1.0: resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= -ansicolors@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" - integrity sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk= - antlr4ts-cli@^0.5.0-alpha.3: version "0.5.0-alpha.3" resolved "https://registry.yarnpkg.com/antlr4ts-cli/-/antlr4ts-cli-0.5.0-alpha.3.tgz#1f581b2a3c840d3921a2f3b1e739e48c7e7c18cd" @@ -9558,7 +9438,7 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -anymatch@^3.0.0, anymatch@^3.0.3, anymatch@^3.1.0, anymatch@~3.1.2: +anymatch@^3.0.0, anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== @@ -9886,7 +9766,7 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= -assert@^1.1.1, assert@^1.4.0: +assert@^1.1.1: version "1.5.0" resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== @@ -10321,11 +10201,6 @@ babel-runtime@6.x, babel-runtime@^6.26.0: core-js "^2.4.0" regenerator-runtime "^0.11.0" -babelify@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/babelify/-/babelify-10.0.0.tgz#fe73b1a22583f06680d8d072e25a1e0d1d1d7fb5" - integrity sha512-X40FaxyH7t3X+JFAKvb1H9wooWKLRCi8pg3m8poqtdZaIng+bjzp9RvKQCvRjF9isHiPkXspbbXT/zwXLtwgwg== - backport@^8.9.2: version "8.9.2" resolved "https://registry.yarnpkg.com/backport/-/backport-8.9.2.tgz#cf0ec69428f9e86c20e1898dd77e8f6c12bf5afa" @@ -10480,12 +10355,12 @@ blob-util@^2.0.2: resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== -bluebird-retry@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/bluebird-retry/-/bluebird-retry-0.11.0.tgz#1289ab22cbbc3a02587baad35595351dd0c1c047" - integrity sha1-EomrIsu8OgJYe6rTVZU1HdDBwEc= +bluebird@3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.1.tgz#df70e302b471d7473489acf26a93d63b53f874de" + integrity sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg== -bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.5.0, bluebird@^3.5.1, bluebird@^3.5.5, bluebird@^3.7.1, bluebird@^3.7.2: +bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.5.5, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -10495,7 +10370,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.9: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== -body-parser@1.19.2, body-parser@^1.18.3: +body-parser@1.19.2: version "1.19.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.2.tgz#4714ccd9c157d44797b8b5607d72c0b89952f26e" integrity sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw== @@ -10615,18 +10490,6 @@ brotli@^1.2.0: dependencies: base64-js "^1.1.2" -browser-pack@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/browser-pack/-/browser-pack-6.1.0.tgz#c34ba10d0b9ce162b5af227c7131c92c2ecd5774" - integrity sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA== - dependencies: - JSONStream "^1.0.3" - combine-source-map "~0.8.0" - defined "^1.0.0" - safe-buffer "^5.1.1" - through2 "^2.0.0" - umd "^3.0.0" - browser-process-hrtime@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" @@ -10639,13 +10502,6 @@ browser-resolve@^1.8.1: dependencies: resolve "1.1.7" -browser-resolve@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-2.0.0.tgz#99b7304cb392f8d73dba741bb2d7da28c6d7842b" - integrity sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ== - dependencies: - resolve "^1.17.0" - browser-stdout@1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" @@ -10711,121 +10567,13 @@ browserify-sign@^4.0.0: inherits "^2.0.1" parse-asn1 "^5.0.0" -browserify-zlib@^0.2.0, browserify-zlib@~0.2.0: +browserify-zlib@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== dependencies: pako "~1.0.5" -browserify@^16.2.3: - version "16.5.2" - resolved "https://registry.yarnpkg.com/browserify/-/browserify-16.5.2.tgz#d926835e9280fa5fd57f5bc301f2ef24a972ddfe" - integrity sha512-TkOR1cQGdmXU9zW4YukWzWVSJwrxmNdADFbqbE3HFgQWe5wqZmOawqZ7J/8MPCwk/W8yY7Y0h+7mOtcZxLP23g== - dependencies: - JSONStream "^1.0.3" - assert "^1.4.0" - browser-pack "^6.0.1" - browser-resolve "^2.0.0" - browserify-zlib "~0.2.0" - buffer "~5.2.1" - cached-path-relative "^1.0.0" - concat-stream "^1.6.0" - console-browserify "^1.1.0" - constants-browserify "~1.0.0" - crypto-browserify "^3.0.0" - defined "^1.0.0" - deps-sort "^2.0.0" - domain-browser "^1.2.0" - duplexer2 "~0.1.2" - events "^2.0.0" - glob "^7.1.0" - has "^1.0.0" - htmlescape "^1.1.0" - https-browserify "^1.0.0" - inherits "~2.0.1" - insert-module-globals "^7.0.0" - labeled-stream-splicer "^2.0.0" - mkdirp-classic "^0.5.2" - module-deps "^6.2.3" - os-browserify "~0.3.0" - parents "^1.0.1" - path-browserify "~0.0.0" - process "~0.11.0" - punycode "^1.3.2" - querystring-es3 "~0.2.0" - read-only-stream "^2.0.0" - readable-stream "^2.0.2" - resolve "^1.1.4" - shasum "^1.0.0" - shell-quote "^1.6.1" - stream-browserify "^2.0.0" - stream-http "^3.0.0" - string_decoder "^1.1.1" - subarg "^1.0.0" - syntax-error "^1.1.1" - through2 "^2.0.0" - timers-browserify "^1.0.1" - tty-browserify "0.0.1" - url "~0.11.0" - util "~0.10.1" - vm-browserify "^1.0.0" - xtend "^4.0.0" - -browserify@^17.0.0: - version "17.0.0" - resolved "https://registry.yarnpkg.com/browserify/-/browserify-17.0.0.tgz#4c48fed6c02bfa2b51fd3b670fddb805723cdc22" - integrity sha512-SaHqzhku9v/j6XsQMRxPyBrSP3gnwmE27gLJYZgMT2GeK3J0+0toN+MnuNYDfHwVGQfLiMZ7KSNSIXHemy905w== - dependencies: - JSONStream "^1.0.3" - assert "^1.4.0" - browser-pack "^6.0.1" - browser-resolve "^2.0.0" - browserify-zlib "~0.2.0" - buffer "~5.2.1" - cached-path-relative "^1.0.0" - concat-stream "^1.6.0" - console-browserify "^1.1.0" - constants-browserify "~1.0.0" - crypto-browserify "^3.0.0" - defined "^1.0.0" - deps-sort "^2.0.1" - domain-browser "^1.2.0" - duplexer2 "~0.1.2" - events "^3.0.0" - glob "^7.1.0" - has "^1.0.0" - htmlescape "^1.1.0" - https-browserify "^1.0.0" - inherits "~2.0.1" - insert-module-globals "^7.2.1" - labeled-stream-splicer "^2.0.0" - mkdirp-classic "^0.5.2" - module-deps "^6.2.3" - os-browserify "~0.3.0" - parents "^1.0.1" - path-browserify "^1.0.0" - process "~0.11.0" - punycode "^1.3.2" - querystring-es3 "~0.2.0" - read-only-stream "^2.0.0" - readable-stream "^2.0.2" - resolve "^1.1.4" - shasum-object "^1.0.0" - shell-quote "^1.6.1" - stream-browserify "^3.0.0" - stream-http "^3.0.0" - string_decoder "^1.1.1" - subarg "^1.0.0" - syntax-error "^1.1.1" - through2 "^2.0.0" - timers-browserify "^1.0.1" - tty-browserify "0.0.1" - url "~0.11.0" - util "~0.12.0" - vm-browserify "^1.0.0" - xtend "^4.0.0" - browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.16.6, browserslist@^4.20.2, browserslist@^4.20.3: version "4.21.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.0.tgz#7ab19572361a140ecd1e023e2c1ed95edda0cefe" @@ -10903,14 +10651,6 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" -buffer@~5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.1.tgz#dd57fa0f109ac59c602479044dca7b8b3d0b71d6" - integrity sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" @@ -11022,11 +10762,6 @@ cacheable-request@^7.0.2: normalize-url "^6.0.1" responselike "^2.0.0" -cached-path-relative@^1.0.0, cached-path-relative@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.2.tgz#a13df4196d26776220cc3356eb147a52dba2c6db" - integrity sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg== - cachedir@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" @@ -11055,25 +10790,6 @@ call-me-maybe@^1.0.1: resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= -caller-callsite@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" - integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= - dependencies: - callsites "^2.0.0" - -caller-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" - integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= - dependencies: - caller-callsite "^2.0.0" - -callsites@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" - integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= - callsites@^3.0.0, callsites@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -11162,14 +10878,6 @@ capture-exit@^2.0.0: dependencies: rsvp "^4.8.4" -cardinal@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-2.1.1.tgz#7cc1055d822d212954d07b085dea251cc7bc5505" - integrity sha1-fMEFXYItISlU0HsIXeolHMe8VQU= - dependencies: - ansicolors "~0.3.2" - redeyed "~2.1.0" - case-sensitive-paths-webpack-plugin@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.3.0.tgz#23ac613cc9a856e4f88ff8bb73bbb5e989825cf7" @@ -11432,11 +11140,6 @@ clean-css@^4.2.3: dependencies: source-map "~0.6.0" -clean-stack@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-1.3.0.tgz#9e821501ae979986c46b1d66d2d432db2fd4ae31" - integrity sha1-noIVAa6XmYbEax1m0tQy2y/UrjE= - clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -11499,33 +11202,6 @@ cli-truncate@^2.1.0: slice-ansi "^3.0.0" string-width "^4.2.0" -cli-ux@^4.9.0: - version "4.9.3" - resolved "https://registry.yarnpkg.com/cli-ux/-/cli-ux-4.9.3.tgz#4c3e070c1ea23eef010bbdb041192e0661be84ce" - integrity sha512-/1owvF0SZ5Gn54cgrikJ0QskgTzeg30HGjkmjFoaHDJzAqFpuX1DBpFR8aLvsE1J5s9MgeYRENQK4BFwOag5VA== - dependencies: - "@oclif/errors" "^1.2.2" - "@oclif/linewrap" "^1.0.0" - "@oclif/screen" "^1.0.3" - ansi-escapes "^3.1.0" - ansi-styles "^3.2.1" - cardinal "^2.1.1" - chalk "^2.4.1" - clean-stack "^2.0.0" - extract-stack "^1.0.0" - fs-extra "^7.0.0" - hyperlinker "^1.0.0" - indent-string "^3.2.0" - is-wsl "^1.1.0" - lodash "^4.17.11" - password-prompt "^1.0.7" - semver "^5.6.0" - strip-ansi "^5.0.0" - supports-color "^5.5.0" - supports-hyperlinks "^1.0.1" - treeify "^1.1.0" - tslib "^1.9.3" - cli-width@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" @@ -11641,19 +11317,6 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= -coffeeify@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/coffeeify/-/coffeeify-3.0.1.tgz#5e2753000c50bd24c693115f33864248dd11136c" - integrity sha512-Qjnr7UX6ldK1PHV7wCnv7AuCd4q19KTUtwJnu/6JRJB4rfm12zvcXtKdacUoePOKr1I4ka/ydKiwWpNAdsQb0g== - dependencies: - convert-source-map "^1.3.0" - through2 "^2.0.0" - -coffeescript@^1.12.7: - version "1.12.7" - resolved "https://registry.yarnpkg.com/coffeescript/-/coffeescript-1.12.7.tgz#e57ee4c4867cf7f606bfc4a0f2d550c0981ddd27" - integrity sha512-pLXHFxQMPklVoEekowk8b3erNynC+DVJzChxS/LCBBgR6/8AJkHivkm//zbowcfc7BTCAjryuhx6gPqPRfsFoA== - collapse-white-space@^1.0.2: version "1.0.6" resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" @@ -11740,7 +11403,7 @@ colorette@^2.0.10, colorette@^2.0.14: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== -colors@1.4.0, colors@^1.3.2: +colors@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== @@ -11753,16 +11416,6 @@ colorspace@1.1.x: color "3.0.x" text-hex "1.0.x" -combine-source-map@^0.8.0, combine-source-map@~0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.8.0.tgz#a58d0df042c186fcf822a8e8015f5450d2d79a8b" - integrity sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos= - dependencies: - convert-source-map "~1.1.0" - inline-source-map "~0.6.0" - lodash.memoize "~3.0.3" - source-map "~0.5.3" - combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz" @@ -11891,7 +11544,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -concat-stream@^1.4.7, concat-stream@^1.5.0, concat-stream@^1.6.0, concat-stream@^1.6.1, concat-stream@~1.6.0: +concat-stream@^1.4.7, concat-stream@^1.5.0, concat-stream@~1.6.0: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -11967,7 +11620,7 @@ console-log-level@^1.4.1: resolved "https://registry.yarnpkg.com/console-log-level/-/console-log-level-1.4.1.tgz#9c5a6bb9ef1ef65b05aba83028b0ff894cdf630a" integrity sha512-VZzbIORbP+PPcN/gg3DXClTLPLg5Slwd5fL2MIc+o1qZ4BXBvWyc6QxPk6T/Mkr6IVjRpoAGf32XxP3ZWMVRcQ== -constants-browserify@^1.0.0, constants-browserify@~1.0.0: +constants-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= @@ -12001,18 +11654,13 @@ contentstream@^1.0.0: dependencies: readable-stream "~1.0.33-1" -convert-source-map@1.X, convert-source-map@^1.1.0, convert-source-map@^1.3.0, convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.5.1, convert-source-map@^1.6.0, convert-source-map@^1.7.0: +convert-source-map@1.X, convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.5.1, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== dependencies: safe-buffer "~5.1.1" -convert-source-map@~1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860" - integrity sha1-SCnId+n+SbMWHzvzZziI4gRpmGA= - cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -12107,24 +11755,6 @@ core-util-is@1.0.2, core-util-is@^1.0.2, core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= -cors@^2.8.4: - version "2.8.5" - resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" - integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== - dependencies: - object-assign "^4" - vary "^1" - -cosmiconfig@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" - integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== - dependencies: - import-fresh "^2.0.0" - is-directory "^0.3.1" - js-yaml "^3.13.1" - parse-json "^4.0.0" - cosmiconfig@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" @@ -12244,7 +11874,7 @@ cross-env@^6.0.3: dependencies: cross-spawn "^7.0.0" -cross-spawn@^6.0.0, cross-spawn@^6.0.5: +cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -12269,7 +11899,7 @@ crypt@~0.0.1: resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= -crypto-browserify@^3.0.0, crypto-browserify@^3.11.0: +crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== @@ -12537,20 +12167,20 @@ cyclist@~0.2.2: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= -cypress-axe@^0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/cypress-axe/-/cypress-axe-0.14.0.tgz#5f5e70fb36b8cb3ba73a8ba01e9262ff1268d5e2" - integrity sha512-7Rdjnko0MjggCmndc1wECAkvQBIhuy+DRtjF7bd5YPZRFvubfMNvrxfqD8PWQmxm7MZE0ffS4Xr43V6ZmvLopg== +cypress-axe@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cypress-axe/-/cypress-axe-1.0.0.tgz#ab4e9486eaa3bb956a90a1ae40d52df42827b4f0" + integrity sha512-QBlNMAd5eZoyhG8RGGR/pLtpHGkvgWXm2tkP68scJ+AjYiNNOlJihxoEwH93RT+rWOLrefw4iWwEx8kpEcrvJA== cypress-file-upload@^5.0.8: version "5.0.8" resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz#d8824cbeaab798e44be8009769f9a6c9daa1b4a1" integrity sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g== -cypress-multi-reporters@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/cypress-multi-reporters/-/cypress-multi-reporters-1.6.0.tgz#2c6833b92e3df412c233657c55009e2d5e1cc7c1" - integrity sha512-JN9yMzDmPwwirzi95N2FC8VJZ0qp+uUJ1ixYHpJFaAtGgIx15LjVmASqQaxnDh8q57jIIJ6C0o7imiLU6N1YNQ== +cypress-multi-reporters@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/cypress-multi-reporters/-/cypress-multi-reporters-1.6.1.tgz#515b891f6c80e0700068efb03ab9d55388399c95" + integrity sha512-FPeC0xWF1N6Myrwc2m7KC0xxlrtG8+x4hlsPFBDRWP8u/veR2x90pGaH3BuJfweV7xoQ4Zo85Qjhu3fgZGrBQQ== dependencies: debug "^4.1.1" lodash "^4.17.15" @@ -12560,27 +12190,27 @@ cypress-pipe@^2.0.0: resolved "https://registry.yarnpkg.com/cypress-pipe/-/cypress-pipe-2.0.0.tgz#577df7a70a8603d89a96dfe4092a605962181af8" integrity sha512-KW9s+bz4tFLucH3rBGfjW+Q12n7S4QpUSSyxiGrgPOfoHlbYWzAGB3H26MO0VTojqf9NVvfd5Kt0MH5XMgbfyg== -cypress-react-selector@^2.3.17: - version "2.3.17" - resolved "https://registry.yarnpkg.com/cypress-react-selector/-/cypress-react-selector-2.3.17.tgz#010382b486c4ec342ab61bcd121bb4ccd79e4590" - integrity sha512-yBi3wv5XUuzcYXearR0PN8lz0kMk44TOP4LVhwK1e4IU1fuWtlyVag1i+2eg0sdKRJ8VePEgmJhgZfOgd3VEgg== +cypress-react-selector@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cypress-react-selector/-/cypress-react-selector-3.0.0.tgz#e86018fffea07ba40c7a1f467a89b475a83cbcae" + integrity sha512-AQCgwbcMDkIdYcf6knvLxqzBnejahIbJPHqUhARi8k+QbM8sgUBDds98PaHJVMdPiX2J8RJjXHmUMPD8VerPSw== dependencies: resq "1.10.2" -cypress-real-events@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.7.0.tgz#ad6a78de33af3af0e6437f5c713e30691c44472c" - integrity sha512-iyXp07j0V9sG3YClVDcvHN2DAQDgr+EjTID82uWDw6OZBlU3pXEBqTMNYqroz3bxlb0k+F74U81aZwzMNaKyew== +cypress-real-events@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.7.1.tgz#8f430d67c29ea4f05b9c5b0311780120cbc9b935" + integrity sha512-/Bg15RgJ0SYsuXc6lPqH08x19z6j2vmhWN4wXfJqm3z8BTAFiK2MvipZPzxT8Z0jJP0q7kuniWrLIvz/i/8lCQ== -cypress-recurse@^1.20.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/cypress-recurse/-/cypress-recurse-1.20.0.tgz#66c09d876ce1c143daa62fea222b5b80067a7fc9" - integrity sha512-8/gqot/XnVkSF8ssgn3zLRTfPw7Bum2tMIOxf6NO+Wqk0MBQdd4NPNVCObllZmmviLsGmF6ZXwlbXZ8TYvD6dw== +cypress-recurse@^1.23.0: + version "1.23.0" + resolved "https://registry.yarnpkg.com/cypress-recurse/-/cypress-recurse-1.23.0.tgz#f87334747516de6737bc4708754e8f429057bc6d" + integrity sha512-CAsdvynhuR3SUEXVJRO2jBEnZRJ6nJp7nMXHwzV4UQq9Lap3Bj72AwcJK0cl51fJXcTaGDXYTQQ9zvGe3TyaQA== -cypress@^9.6.1: - version "9.6.1" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.6.1.tgz#a7d6b5a53325b3dc4960181f5800a5ade0f085eb" - integrity sha512-ECzmV7pJSkk+NuAhEw6C3D+RIRATkSb2VAHXDY6qGZbca/F9mv5pPsj2LO6Ty6oIFVBTrwCyL9agl28MtJMe2g== +cypress@^10.7.0: + version "10.7.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-10.7.0.tgz#2d37f8b9751c6de33ee48639cb7e67a2ce593231" + integrity sha512-gTFvjrUoBnqPPOu9Vl5SBHuFlzx/Wxg/ZXIz2H4lzoOLFelKeF7mbwYUOzgzgF0oieU2WhJAestQdkgwJMMTvQ== dependencies: "@cypress/request" "^2.88.10" "@cypress/xvfb" "^1.2.4" @@ -13117,20 +12747,6 @@ debug@4.3.1: dependencies: ms "2.1.2" -debug@4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" - integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== - dependencies: - ms "2.1.2" - -debug@4.3.3: - version "4.3.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== - dependencies: - ms "2.1.2" - debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -13240,7 +12856,7 @@ deep-object-diff@^1.1.0: resolved "https://registry.yarnpkg.com/deep-object-diff/-/deep-object-diff-1.1.0.tgz#d6fabf476c2ed1751fc94d5ca693d2ed8c18bc5a" integrity sha512-b+QLs5vHgS+IoSNcUE4n9HP2NwcHj7aqnJWsjPtuG75Rh5TOaGt0OjAYInh77d5T16V5cRDC+Pw/6ZZZiETBGw== -deepmerge@3.2.0, deepmerge@^2.1.1, deepmerge@^4.0.0, deepmerge@^4.2.2: +deepmerge@3.2.0, deepmerge@^2.1.1, deepmerge@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== @@ -13398,16 +13014,6 @@ deprecation@^2.0.0, deprecation@^2.3.1: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== -deps-sort@^2.0.0, deps-sort@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/deps-sort/-/deps-sort-2.0.1.tgz#9dfdc876d2bcec3386b6829ac52162cda9fa208d" - integrity sha512-1orqXQr5po+3KI6kQb9A4jnXT1PBwggGl2d7Sq2xsnOeI9GPcE/tGcF9UiSZtZBM7MukY4cAh7MemS6tZYipfw== - dependencies: - JSONStream "^1.0.3" - shasum-object "^1.0.0" - subarg "^1.0.0" - through2 "^2.0.0" - des.js@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" @@ -13466,7 +13072,7 @@ detect-port@^1.3.0: address "^1.0.1" debug "^2.6.0" -detective@^5.0.2, detective@^5.2.0: +detective@^5.0.2: version "5.2.0" resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b" integrity sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg== @@ -13475,11 +13081,6 @@ detective@^5.0.2, detective@^5.2.0: defined "^1.0.0" minimist "^1.1.1" -devtools-protocol@0.0.818844: - version "0.0.818844" - resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.818844.tgz#d1947278ec85b53e4c8ca598f607a28fa785ba9e" - integrity sha512-AD1hi7iVJ8OD0aMLQU5VK0XH9LDlA1+BcPIgrAxPfaibx2DbWucuyOhc4oyQCbnvDDO68nN6/LcKfqTP343Jjg== - devtools-protocol@0.0.901419: version "0.0.901419" resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.901419.tgz#79b5459c48fe7e1c5563c02bd72f8fec3e0cebcd" @@ -13647,7 +13248,7 @@ dom-walk@^0.1.0: resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018" integrity sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg= -domain-browser@^1.1.1, domain-browser@^1.2.0: +domain-browser@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== @@ -13739,7 +13340,7 @@ dotenv@^16.0.1: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d" integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ== -dotenv@^8.0.0, dotenv@^8.1.0: +dotenv@^8.0.0: version "8.2.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== @@ -13787,7 +13388,7 @@ dpdm@3.5.0: typescript "^3.5.3" yargs "^13.3.0" -duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2, duplexer2@~0.1.4: +duplexer2@~0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME= @@ -14253,11 +13854,6 @@ es6-map@^0.1.5: es6-symbol "~3.1.1" event-emitter "~0.3.5" -es6-promise-pool@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/es6-promise-pool/-/es6-promise-pool-2.5.0.tgz#147c612b36b47f105027f9d2bf54a598a99d9ccb" - integrity sha1-FHxhKza0fxBQJ/nSv1SlmKmdnMs= - es6-promise@^4.0.3, es6-promise@^4.2.8: version "4.2.8" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" @@ -14661,7 +14257,7 @@ espree@^7.3.0, espree@^7.3.1: acorn-jsx "^5.3.1" eslint-visitor-keys "^1.3.0" -esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: +esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== @@ -14753,11 +14349,6 @@ events@^1.0.2: resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= -events@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/events/-/events-2.1.0.tgz#2a9a1e18e6106e0e812aa9ebd4a819b3c29c0ba5" - integrity sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg== - events@^3.0.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -14911,7 +14502,7 @@ expose-loader@^0.7.5: resolved "https://registry.yarnpkg.com/expose-loader/-/expose-loader-0.7.5.tgz#e29ea2d9aeeed3254a3faa1b35f502db9f9c3f6f" integrity sha512-iPowgKUZkTPX5PznYsmifVj9Bob0w2wTHVkt/eYNPSzyebkUgIedmskf/kcfEIWpiWjg3JRjnW+a17XypySMuw== -express@^4.16.3, express@^4.17.1, express@^4.17.3: +express@^4.17.1, express@^4.17.3: version "4.17.3" resolved "https://registry.yarnpkg.com/express/-/express-4.17.3.tgz#f6c7302194a4fb54271b73a1fe7a06478c8f85a1" integrity sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg== @@ -14990,12 +14581,7 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-stack@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/extract-stack/-/extract-stack-1.0.0.tgz#b97acaf9441eea2332529624b732fc5a1c8165fa" - integrity sha1-uXrK+UQe6iMyUpYktzL8WhyBZfo= - -extract-zip@2.0.1, extract-zip@^2.0.0, extract-zip@^2.0.1: +extract-zip@2.0.1, extract-zip@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== @@ -15409,11 +14995,6 @@ folktale@2.3.2: resolved "https://registry.yarnpkg.com/folktale/-/folktale-2.3.2.tgz#38231b039e5ef36989920cbf805bf6b227bf4fd4" integrity sha512-+8GbtQBwEqutP0v3uajDDoN64K2ehmHd0cjlghhxh0WpcfPzAIjPA03e1VvHlxL02FVGR0A6lwXsNQKn3H1RNQ== -follow-redirects@1.12.1: - version "1.12.1" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.12.1.tgz#de54a6205311b93d60398ebc01cf7015682312b6" - integrity sha512-tmRv0AVuR7ZyouUHLeNSiO6pqulF7dYa3s19c6t+wz9LD69/uSzdMxJ2S91nTI9U3rt/IldxpzMOFejp6f0hjg== - follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.14.9: version "1.15.0" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.0.tgz#06441868281c86d0dda4ad8bdaead2d02dca89d4" @@ -15441,11 +15022,6 @@ foreach@^2.0.5: resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= -foreachasync@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/foreachasync/-/foreachasync-3.0.0.tgz#5502987dc8714be3392097f32e0071c9dee07cf6" - integrity sha1-VQKYfchxS+M5IJfzLgBxyd7gfPY= - foreground-child@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-2.0.0.tgz#71b32800c9f15aa8f2f83f4a6bd9bff35d861a53" @@ -15611,7 +15187,7 @@ fs-extra@^10.0.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@^7.0.0, fs-extra@^7.0.1: +fs-extra@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== @@ -15767,11 +15343,6 @@ geckodriver@^3.0.2: https-proxy-agent "5.0.0" tar "6.1.11" -generic-pool@^3.7.1: - version "3.7.1" - resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.7.1.tgz#36fe5bb83e7e0e032e5d32cd05dc00f5ff119aa8" - integrity sha512-ug6DAZoNgWm6q5KhPFA+hzXfBLFQu5sTXxPpv44DmE0A2g+CiHoq9LTVdkXpZMkYVMoGw83F6W+WT0h0MFMK/w== - gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -15790,7 +15361,7 @@ geojson-vt@^3.2.1: resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-3.2.1.tgz#f8adb614d2c1d3f6ee7c4265cad4bbf3ad60c8b7" integrity sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg== -get-assigned-identifiers@^1.1.0, get-assigned-identifiers@^1.2.0: +get-assigned-identifiers@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz#6dbf411de648cbaf8d9169ebb0d2d576191e2ff1" integrity sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ== @@ -16012,7 +15583,7 @@ glob@^6.0.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0, glob@~7.2.0: +glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0, glob@~7.2.0: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -16378,11 +15949,6 @@ has-bigints@^1.0.1: resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== -has-flag@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" - integrity sha1-6CB68cx7MNRGzHC3NLXovhj4jVE= - has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -16790,11 +16356,6 @@ html@1.0.0: dependencies: concat-stream "^1.4.7" -htmlescape@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351" - integrity sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E= - htmlparser2@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-4.1.0.tgz#9a4ef161f2e4625ebf7dfbe6c0a2f52d18a59e78" @@ -16949,14 +16510,6 @@ https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0: agent-base "6" debug "4" -https-proxy-agent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b" - integrity sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg== - dependencies: - agent-base "5" - debug "4" - human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -16974,11 +16527,6 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -hyperlinker@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/hyperlinker/-/hyperlinker-1.0.0.tgz#23dc9e38a206b208ee49bc2d6c8ef47027df0c0e" - integrity sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ== - hyphenate-style-name@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48" @@ -17047,13 +16595,6 @@ ignore@^5.0.5, ignore@^5.1.1, ignore@^5.1.4, ignore@^5.1.8, ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== -image-size@^0.8.2: - version "0.8.3" - resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.8.3.tgz#f0b568857e034f29baffd37013587f2c0cad8b46" - integrity sha512-SMtq1AJ+aqHB45c3FsB4ERK0UCiA2d3H1uq8s+8T0Pf8A3W4teyBQyaFaktH6xvZqh+npwlKU7i4fJo0r7TYTg== - dependencies: - queue "6.0.1" - immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -17064,14 +16605,6 @@ immer@^9.0.15, immer@^9.0.7: resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.15.tgz#0b9169e5b1d22137aba7d43f8a81a495dd1b62dc" integrity sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ== -import-fresh@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" - integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= - dependencies: - caller-path "^2.0.0" - resolve-from "^3.0.0" - import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" @@ -17103,7 +16636,7 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= -indent-string@^3.0.0, indent-string@^3.2.0: +indent-string@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= @@ -17131,7 +16664,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -17156,13 +16689,6 @@ ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== -inline-source-map@~0.6.0: - version "0.6.2" - resolved "https://registry.yarnpkg.com/inline-source-map/-/inline-source-map-0.6.2.tgz#f9393471c18a79d1724f863fa38b586370ade2a5" - integrity sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU= - dependencies: - source-map "~0.5.3" - inline-style-parser@0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" @@ -17216,22 +16742,6 @@ inquirer@^8.2.3: through "^2.3.6" wrap-ansi "^7.0.0" -insert-module-globals@^7.0.0, insert-module-globals@^7.2.1: - version "7.2.1" - resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.2.1.tgz#d5e33185181a4e1f33b15f7bf100ee91890d5cb3" - integrity sha512-ufS5Qq9RZN+Bu899eA9QCAYThY+gGW7oRkmb0vC93Vlyu/CFGcH0OYPEjVkDXA5FEbTt1+VWzdoOD3Ny9N+8tg== - dependencies: - JSONStream "^1.0.3" - acorn-node "^1.5.2" - combine-source-map "^0.8.0" - concat-stream "^1.6.1" - is-buffer "^1.1.0" - path-is-absolute "^1.0.1" - process "~0.11.0" - through2 "^2.0.0" - undeclared-identifiers "^1.1.2" - xtend "^4.0.0" - install-artifact-from-github@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.3.0.tgz#cab6ff821976b8a35b0c079da19a727c90381a40" @@ -17421,7 +16931,7 @@ is-boolean-object@^1.0.1, is-boolean-object@^1.1.0: dependencies: call-bind "^1.0.0" -is-buffer@^1.0.2, is-buffer@^1.1.0, is-buffer@^1.1.5, is-buffer@~1.1.1: +is-buffer@^1.0.2, is-buffer@^1.1.5, is-buffer@~1.1.1: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== @@ -17499,11 +17009,6 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: is-data-descriptor "^1.0.0" kind-of "^6.0.2" -is-directory@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" - integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= - is-docker@^2.0.0, is-docker@^2.1.1: version "2.2.1" resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" @@ -17568,11 +17073,6 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.0.0.tgz#038c31b774709641bda678b1f06a4e3227c10b3e" integrity sha512-elzyIdM7iKoFHzcrndIqjYomImhxrFRnGP3galODoII4TB9gI7mZ+FnlLQmmjf27SxHS2gKEeyhX5/+YRS6H9g== -is-generator-function@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" - integrity sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw== - is-glob@^3.0.0, is-glob@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" @@ -18686,7 +18186,7 @@ jquery@^3.5.0: resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470" integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw== -js-base64@^2.4.3: +js-base64@^2.4.9: version "2.5.2" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.2.tgz#313b6274dda718f714d00b3330bbae6e38e90209" integrity sha512-Vg8czh0Q7sFBSUMWWArX/miJeBWYBPpdU/3M/DKSaekLMqrqVPaedp+5mZhie/r0lgrcaYBfwXatEew6gwgiQQ== @@ -18841,13 +18341,6 @@ json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: dependencies: jsonify "~0.0.0" -json-stable-stringify@~0.0.0: - version "0.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz#611c23e814db375527df851193db59dd2af27f45" - integrity sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U= - dependencies: - jsonify "~0.0.0" - json-stringify-pretty-compact@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-1.2.0.tgz#0bc316b5e6831c07041fc35612487fb4e9ab98b8" @@ -18952,11 +18445,6 @@ jsprim@^2.0.2: json-schema "0.4.0" verror "1.10.0" -jssha@^2.1.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/jssha/-/jssha-2.3.1.tgz#147b2125369035ca4b2f7d210dc539f009b3de9a" - integrity sha1-FHshJTaQNcpLL30hDcU58Amz3po= - jsts@^1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/jsts/-/jsts-1.6.2.tgz#c0efc885edae06ae84f78cbf2a0110ba929c5925" @@ -19104,14 +18592,6 @@ kuler@^2.0.0: resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== -labeled-stream-splicer@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz#42a41a16abcd46fd046306cf4f2c3576fffb1c21" - integrity sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw== - dependencies: - inherits "^2.0.1" - stream-splicer "^2.0.0" - language-subtag-registry@~0.3.2: version "0.3.21" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a" @@ -19395,21 +18875,11 @@ lodash-es@^4.17.21: resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== -lodash._reinterpolate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" - integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= - lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= -lodash.clonedeep@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= - lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -19500,11 +18970,6 @@ lodash.memoize@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= -lodash.memoize@~3.0.3: - version "3.0.4" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f" - integrity sha1-LcvSwofLwKVcxCMovQxzYVDVPj8= - lodash.merge@4.6.2, lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -19525,21 +18990,6 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= -lodash.template@^4.4.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" - integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== - dependencies: - lodash._reinterpolate "^3.0.0" - lodash.templatesettings "^4.0.0" - -lodash.templatesettings@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" - integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== - dependencies: - lodash._reinterpolate "^3.0.0" - lodash.toarray@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" @@ -20266,7 +19716,7 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: +minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== @@ -20453,27 +19903,6 @@ mock-fs@^5.1.2: resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.1.2.tgz#6fa486e06d00f8793a8d2228de980eff93ce6db7" integrity sha512-YkjQkdLulFrz0vD4BfNQdQRVmgycXTV7ykuHMlyv+C8WCHazpkiQRDthwa02kSyo8wKnY9wRptHfQLgmf0eR+A== -module-deps@^6.2.3: - version "6.2.3" - resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-6.2.3.tgz#15490bc02af4b56cf62299c7c17cba32d71a96ee" - integrity sha512-fg7OZaQBcL4/L+AK5f4iVqf9OMbCclXfy/znXRxTVhJSeW5AIlS9AwheYwDaXM3lVW7OBeaeUEY3gbaC6cLlSA== - dependencies: - JSONStream "^1.0.3" - browser-resolve "^2.0.0" - cached-path-relative "^1.0.2" - concat-stream "~1.6.0" - defined "^1.0.0" - detective "^5.2.0" - duplexer2 "^0.1.2" - inherits "^2.0.1" - parents "^1.0.0" - readable-stream "^2.0.2" - resolve "^1.4.0" - stream-combiner2 "^1.1.1" - subarg "^1.0.0" - through2 "^2.0.0" - xtend "^4.0.0" - module-details-from-path@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" @@ -20925,10 +20354,10 @@ node-releases@^2.0.5: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.5.tgz#280ed5bc3eba0d96ce44897d8aee478bfb3d9666" integrity sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q== -node-sass@7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-7.0.1.tgz#ad4f6bc663de8acc0a9360db39165a1e2620aa72" - integrity sha512-uMy+Xt29NlqKCFdFRZyXKOTqGt+QaKHexv9STj2WeLottnlqZEEWx6Bj0MXNthmFRRdM/YwyNo/8Tr46TOM0jQ== +node-sass@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-7.0.3.tgz#7620bcd5559c2bf125c4fbb9087ba75cd2df2ab2" + integrity sha512-8MIlsY/4dXUkJDYht9pIWBhMil3uHmE8b/AdJPjmFn1nBx9X9BASzfzmsCy0uCCb8eqI3SYYzVPDswWqSx7gjw== dependencies: async-foreach "^0.1.3" chalk "^4.1.2" @@ -20942,7 +20371,7 @@ node-sass@7.0.1: node-gyp "^8.4.1" npmlog "^5.0.0" request "^2.88.0" - sass-graph "4.0.0" + sass-graph "^4.0.1" stdout-stream "^1.4.0" "true-case-path" "^1.0.2" @@ -21162,7 +20591,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@4.X, object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@4.X, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -21481,7 +20910,7 @@ original-url@^1.2.3: dependencies: forwarded-parse "^2.1.0" -os-browserify@^0.3.0, os-browserify@~0.3.0: +os-browserify@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= @@ -21524,13 +20953,6 @@ ospath@^1.2.2: resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs= -outpipe@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/outpipe/-/outpipe-1.1.1.tgz#50cf8616365e87e031e29a5ec9339a3da4725fa2" - integrity sha1-UM+GFjZeh+Ax4ppeyTOaPaRyX6I= - dependencies: - shell-quote "^1.4.2" - overlayscrollbars@^1.13.1: version "1.13.1" resolved "https://registry.yarnpkg.com/overlayscrollbars/-/overlayscrollbars-1.13.1.tgz#0b840a88737f43a946b9d87875a2f9e421d0338a" @@ -21742,13 +21164,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parents@^1.0.0, parents@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parents/-/parents-1.0.1.tgz#fedd4d2bf193a77745fe71e371d73c3307d9c751" - integrity sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E= - dependencies: - path-platform "~0.11.15" - parse-asn1@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712" @@ -21842,20 +21257,12 @@ pascalcase@^0.1.1: resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= -password-prompt@^1.0.7: - version "1.1.2" - resolved "https://registry.yarnpkg.com/password-prompt/-/password-prompt-1.1.2.tgz#85b2f93896c5bd9e9f2d6ff0627fa5af3dc00923" - integrity sha512-bpuBhROdrhuN3E7G/koAju0WjVw9/uQOG5Co5mokNj0MiOSBVZS1JTwM4zl55hu0WFmIEFvO9cU9sJQiBIYeIA== - dependencies: - ansi-escapes "^3.1.0" - cross-spawn "^6.0.5" - -path-browserify@0.0.1, path-browserify@~0.0.0: +path-browserify@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== -path-browserify@^1.0.0, path-browserify@^1.0.1: +path-browserify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== @@ -21875,7 +21282,7 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: +path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= @@ -21900,11 +21307,6 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-platform@~0.11.15: - version "0.11.15" - resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2" - integrity sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I= - path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" @@ -21980,21 +21382,6 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= -percy-client@^3.2.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/percy-client/-/percy-client-3.7.0.tgz#780e7d780c7f646e59ffb6ee9d3d16e8237851ff" - integrity sha512-5levWR/nfVuSDL9YPN9Sn1M41I2/FmC/FndhD84s6W+mrVC4mB0cc9cT9F58hLuh7/133I/YvyI9Vc6NN41+2g== - dependencies: - bluebird "^3.5.1" - bluebird-retry "^0.11.0" - dotenv "^8.1.0" - es6-promise-pool "^2.5.0" - jssha "^2.1.0" - regenerator-runtime "^0.13.1" - request "^2.87.0" - request-promise "^4.2.2" - walk "^2.3.14" - performance-now@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" @@ -22727,7 +22114,7 @@ process-on-spawn@^1.0.0: dependencies: fromentries "^1.2.0" -process@^0.11.10, process@~0.11.0: +process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= @@ -22742,7 +22129,7 @@ progress@^1.1.8: resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74= -progress@^2.0.0, progress@^2.0.1, progress@^2.0.3: +progress@^2.0.0, progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -22899,7 +22286,7 @@ proxy-from-env@1.0.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4= -proxy-from-env@1.1.0, proxy-from-env@^1.0.0, proxy-from-env@^1.1.0: +proxy-from-env@1.1.0, proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== @@ -22979,7 +22366,7 @@ punycode@1.3.2: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= -punycode@^1.2.4, punycode@^1.3.2: +punycode@^1.2.4: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= @@ -23014,24 +22401,6 @@ puppeteer@^10.2.0: unbzip2-stream "1.3.3" ws "7.4.6" -puppeteer@^5.3.1: - version "5.5.0" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-5.5.0.tgz#331a7edd212ca06b4a556156435f58cbae08af00" - integrity sha512-OM8ZvTXAhfgFA7wBIIGlPQzvyEETzDjeRa4mZRCRHxYL+GNH5WAuYUQdja3rpWZvkX/JKqmuVgbsxDNsDFjMEg== - dependencies: - debug "^4.1.0" - devtools-protocol "0.0.818844" - extract-zip "^2.0.0" - https-proxy-agent "^4.0.0" - node-fetch "^2.6.1" - pkg-dir "^4.2.0" - progress "^2.0.1" - proxy-from-env "^1.0.0" - rimraf "^3.0.2" - tar-fs "^2.0.0" - unbzip2-stream "^1.3.3" - ws "^7.2.3" - q@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -23063,7 +22432,7 @@ query-string@^6.13.2: split-on-first "^1.0.0" strict-uri-encode "^2.0.0" -querystring-es3@^0.2.0, querystring-es3@~0.2.0: +querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= @@ -23078,13 +22447,6 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== -queue@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.1.tgz#abd5a5b0376912f070a25729e0b6a7d565683791" - integrity sha512-AJBQabRCCNr9ANq8v77RJEv73DPbn55cdTb+Giq4X0AVnNVZvMHlYp7XlQiN+1npCZj1DuSmaA2hYVUUDgxFDg== - dependencies: - inherits "~2.0.3" - quick-format-unescaped@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.3.tgz#6d6b66b8207aa2b35eef12be1421bb24c428f652" @@ -23871,13 +23233,6 @@ read-installed@~4.0.3: optionalDependencies: graceful-fs "^4.1.2" -read-only-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0" - integrity sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A= - dependencies: - readable-stream "^2.0.2" - read-package-json@^2.0.0, read-package-json@^2.0.10: version "2.1.1" resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.1.1.tgz#16aa66c59e7d4dad6288f179dd9295fd59bb98f1" @@ -23949,7 +23304,7 @@ readable-stream@1.0, "readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0 isarray "0.0.1" string_decoder "~0.10.x" -"readable-stream@2 || 3", readable-stream@3, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: +"readable-stream@2 || 3", readable-stream@3, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -24019,13 +23374,6 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -redeyed@~2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-2.1.1.tgz#8984b5815d99cb220469c99eeeffe38913e6cc0b" - integrity sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs= - dependencies: - esprima "~4.0.0" - reduce-reducers@*, reduce-reducers@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-1.0.4.tgz#fb77e751a9eb0201760ac5a605ca8c9c2d0537f8" @@ -24132,10 +23480,10 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -regenerator-runtime@^0.13.1, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7: - version "0.13.9" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" - integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== +regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7: + version "0.13.7" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== regenerator-transform@^0.15.0: version "0.15.0" @@ -24441,13 +23789,6 @@ request-progress@^3.0.0: dependencies: throttleit "^1.0.0" -request-promise-core@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346" - integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag== - dependencies: - lodash "^4.17.11" - request-promise-core@1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f" @@ -24464,17 +23805,7 @@ request-promise-native@^1.0.8: stealthy-require "^1.1.1" tough-cookie "^2.3.3" -request-promise@^4.2.2: - version "4.2.4" - resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.4.tgz#1c5ed0d71441e38ad58c7ce4ea4ea5b06d54b310" - integrity sha512-8wgMrvE546PzbR5WbYxUQogUnUDfM0S7QIFZMID+J73vdFARkFy+HElj4T+MWYhpXwlLp0EQ8Zoj8xUA0he4Vg== - dependencies: - bluebird "^3.5.0" - request-promise-core "1.1.2" - stealthy-require "^1.1.1" - tough-cookie "^2.3.3" - -request@^2.44.0, request@^2.87.0, request@^2.88.0, request@^2.88.2: +request@^2.44.0, request@^2.88.0, request@^2.88.2: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -24556,11 +23887,6 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -24600,7 +23926,7 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.1.4, resolve@^1.1.5, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.9.0: +resolve@^1.1.5, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1, resolve@^1.3.2, resolve@^1.9.0: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -24667,11 +23993,6 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -retry-axios@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/retry-axios/-/retry-axios-1.0.1.tgz#c1e465126416d8aee7a0a2d4be28401cc0135029" - integrity sha512-aVnENElFbdmbsv1WbTi610Ukdper88yUPz4Y3eg/DUyHV7vNaLrj9orB6FOjvmFoXL9wZvbMAsOD87BmcyBVOw== - retry@0.12.0, retry@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" @@ -24890,14 +24211,14 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -sass-graph@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-4.0.0.tgz#fff8359efc77b31213056dfd251d05dadc74c613" - integrity sha512-WSO/MfXqKH7/TS8RdkCX3lVkPFQzCgbqdGsmSKq6tlPU+GpGEsa/5aW18JqItnqh+lPtcjifqdZ/VmiILkKckQ== +sass-graph@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-4.0.1.tgz#2ff8ca477224d694055bf4093f414cf6cfad1d2e" + integrity sha512-5YCfmGBmxoIRYHnKK2AKzrAkCoQ8ozO+iumT8K4tXJXRVCPf+7s1/9KxTSW3Rbvf+7Y7b4FR3mWyLnQr3PHocA== dependencies: glob "^7.0.0" lodash "^4.17.11" - scss-tokenizer "^0.3.0" + scss-tokenizer "^0.4.3" yargs "^17.2.1" sass-loader@^10.3.1: @@ -25007,13 +24328,13 @@ screenfull@^5.0.0: resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.0.0.tgz#5c2010c0e84fd4157bf852877698f90b8cbe96f6" integrity sha512-yShzhaIoE9OtOhWVyBBffA6V98CDCoyHTsp8228blmqYy1Z5bddzE/4FPiJKlr8DVR4VBiiUyfPzIQPIYDkeMA== -scss-tokenizer@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.3.0.tgz#ef7edc3bc438b25cd6ffacf1aa5b9ad5813bf260" - integrity sha512-14Zl9GcbBvOT9057ZKjpz5yPOyUWG2ojd9D5io28wHRYsOrs7U95Q+KNL87+32p8rc+LvDpbu/i9ZYjM9Q+FsQ== +scss-tokenizer@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.4.3.tgz#1058400ee7d814d71049c29923d2b25e61dc026c" + integrity sha512-raKLgf1LI5QMQnG+RxHz6oK0sL3x3I4FN2UDLqgLOGO8hodECNnNh5BXn7fAyBxrA8zVzdQizQ6XjNJQ+uBwMw== dependencies: - js-base64 "^2.4.3" - source-map "^0.7.1" + js-base64 "^2.4.9" + source-map "^0.7.3" secure-json-parse@^2.4.0: version "2.4.0" @@ -25216,7 +24537,7 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -sha.js@^2.4.0, sha.js@^2.4.8, sha.js@~2.4.4: +sha.js@^2.4.0, sha.js@^2.4.8: version "2.4.11" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== @@ -25260,21 +24581,6 @@ sharp@^0.30.1: tar-fs "^2.1.1" tunnel-agent "^0.6.0" -shasum-object@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shasum-object/-/shasum-object-1.0.0.tgz#0b7b74ff5b66ecf9035475522fa05090ac47e29e" - integrity sha512-Iqo5rp/3xVi6M4YheapzZhhGPVs0yZwHj7wvwQ1B9z8H6zk+FEnI7y3Teq7qwnekfEhu8WmG2z0z4iWZaxLWVg== - dependencies: - fast-safe-stringify "^2.0.7" - -shasum@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/shasum/-/shasum-1.0.2.tgz#e7012310d8f417f4deb5712150e5678b87ae565f" - integrity sha1-5wEjENj0F/TetXEhUOVni4euVl8= - dependencies: - json-stable-stringify "~0.0.0" - sha.js "~2.4.4" - shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -25299,11 +24605,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@^1.4.2, shell-quote@^1.6.1: - version "1.7.2" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" - integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== - shellwords@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" @@ -25603,7 +24904,7 @@ source-map@0.5.6: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" integrity sha1-dc449SvwczxafwwRjYEzSiu19BI= -source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.3: +source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.6, source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= @@ -25613,7 +24914,7 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.7.1, source-map@^0.7.3: +source-map@^0.7.3: version "0.7.3" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== @@ -25930,7 +25231,7 @@ store2@^2.12.0: resolved "https://registry.yarnpkg.com/store2/-/store2-2.12.0.tgz#e1f1b7e1a59b6083b2596a8d067f6ee88fd4d3cf" integrity sha512-7t+/wpKLanLzSnQPX8WAcuLCCeuSHoWdQuh9SB3xD0kNOM38DNf+0Oa+wmvxmYueRzkmh6IcdKFtvTa+ecgPDw== -stream-browserify@^2.0.0, stream-browserify@^2.0.1: +stream-browserify@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== @@ -25938,14 +25239,6 @@ stream-browserify@^2.0.0, stream-browserify@^2.0.1: inherits "~2.0.1" readable-stream "^2.0.2" -stream-browserify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" - integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== - dependencies: - inherits "~2.0.4" - readable-stream "^3.5.0" - stream-chopper@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/stream-chopper/-/stream-chopper-3.0.1.tgz#73791ae7bf954c297d6683aec178648efc61dd75" @@ -25953,14 +25246,6 @@ stream-chopper@^3.0.1: dependencies: readable-stream "^3.0.6" -stream-combiner2@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/stream-combiner2/-/stream-combiner2-1.1.1.tgz#fb4d8a1420ea362764e21ad4780397bebcb41cbe" - integrity sha1-+02KFCDqNidk4hrUeAOXvry0HL4= - dependencies: - duplexer2 "~0.1.0" - readable-stream "^2.0.2" - stream-each@^1.1.0: version "1.2.3" resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" @@ -25980,16 +25265,6 @@ stream-http@^2.7.2: to-arraybuffer "^1.0.0" xtend "^4.0.0" -stream-http@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-3.1.1.tgz#0370a8017cf8d050b9a8554afe608f043eaff564" - integrity sha512-S7OqaYu0EkFpgeGFb/NPOoPLxFko7TPqtEeFg5DXPB4v/KETHG0Ln6fRFrNezoelpaDKmycEmmZ81cC9DAwgYg== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.4" - readable-stream "^3.6.0" - xtend "^4.0.2" - stream-shift@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" @@ -26000,14 +25275,6 @@ stream-slicer@0.0.6: resolved "https://registry.yarnpkg.com/stream-slicer/-/stream-slicer-0.0.6.tgz#f86b2ac5c2440b7a0a87b71f33665c0788046138" integrity sha1-+GsqxcJEC3oKh7cfM2ZcB4gEYTg= -stream-splicer@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/stream-splicer/-/stream-splicer-2.0.1.tgz#0b13b7ee2b5ac7e0609a7463d83899589a363fcd" - integrity sha512-Xizh4/NPuYSyAXyT7g8IvdJ9HJpxIGL9PjyhtywCZvvP0OPIdqyrr4dMikeuvY8xahpdKEBlBTySe583totajg== - dependencies: - inherits "^2.0.1" - readable-stream "^2.0.2" - stream-to-async-iterator@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/stream-to-async-iterator/-/stream-to-async-iterator-0.2.0.tgz#bef5c885e9524f98b2fa5effecc357bd58483780" @@ -26342,13 +25609,6 @@ stylis@4.0.13: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91" integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag== -subarg@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" - integrity sha1-9izxdYHplrSPyWVpn1TAauJouNI= - dependencies: - minimist "^1.1.0" - success-symbol@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/success-symbol/-/success-symbol-0.1.0.tgz#24022e486f3bf1cdca094283b769c472d3b72897" @@ -26413,7 +25673,7 @@ supports-color@^2.0.0: resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= -supports-color@^5.0.0, supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: +supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -26427,14 +25687,6 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-hyperlinks@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz#71daedf36cc1060ac5100c351bb3da48c29c0ef7" - integrity sha512-HHi5kVSefKaJkGYXbDuKbUGRVxqnWGn3J2e39CYcNJEfWciGq2zYtOhXLTlvrOZW1QU7VX67w7fMmWafHX9Pfw== - dependencies: - has-flag "^2.0.0" - supports-color "^5.0.0" - supports-hyperlinks@^2.0.0, supports-hyperlinks@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb" @@ -26500,13 +25752,6 @@ synchronous-promise@^2.0.15: resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.15.tgz#07ca1822b9de0001f5ff73595f3d08c4f720eb8e" integrity sha512-k8uzYIkIVwmT+TcglpdN50pS2y1BDcUnBPK9iJeGu0Pl1lOI8pD6wtzgw91Pjpe+RxtTncw32tLxs/R0yNL2Mg== -syntax-error@^1.1.1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.4.0.tgz#2d9d4ff5c064acb711594a3e3b95054ad51d907c" - integrity sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w== - dependencies: - acorn-node "^1.2.0" - tabbable@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.2.1.tgz#e3fda7367ddbb172dcda9f871c0fdb36d1c4cd9c" @@ -26801,13 +26046,6 @@ timed-out@^2.0.0: resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-2.0.0.tgz#f38b0ae81d3747d628001f41dafc652ace671c0a" integrity sha1-84sK6B03R9YoAB9B2vxlKs5nHAo= -timers-browserify@^1.0.1: - version "1.4.2" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-1.4.2.tgz#c9c58b575be8407375cb5e2462dacee74359f41d" - integrity sha1-ycWLV1voQHN1y14kYtrO50NZ9B0= - dependencies: - process "~0.11.0" - timers-browserify@^2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.6.tgz#241e76927d9ca05f4d959819022f5b3664b64bae" @@ -27177,7 +26415,7 @@ tslib@2.3.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== -tslib@^1, tslib@^1.0.0, tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: +tslib@^1.0.0, tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.13.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== @@ -27204,11 +26442,6 @@ tty-browserify@0.0.0: resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= -tty-browserify@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811" - integrity sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw== - tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -27334,11 +26567,6 @@ uglify-js@^3.1.4, uglify-js@^3.14.3: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.4.tgz#68756f17d1b90b9d289341736cb9a567d6882f90" integrity sha512-AbiSR44J0GoCeV81+oxcy/jDOElO2Bx3d0MfQCUShq7JRXaM4KtQopZsq2vFv8bCq2yMaGrw1FgygUd03RyRDA== -umd@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.3.tgz#aa9fe653c42b9097678489c01000acb69f0b26cf" - integrity sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow== - unbox-primitive@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" @@ -27357,30 +26585,11 @@ unbzip2-stream@1.3.3: buffer "^5.2.1" through "^2.3.8" -unbzip2-stream@^1.3.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" - integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== - dependencies: - buffer "^5.2.1" - through "^2.3.8" - unc-path-regex@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= -undeclared-identifiers@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz#9254c1d37bdac0ac2b52de4b6722792d2a91e30f" - integrity sha512-pJOW4nxjlmfwKApE4zvxLScM/njmwj/DiUBv7EabwE4O8kRUy+HIwxQtZLBPll/jx1LJyBcqNfB3/cpv9EZwOw== - dependencies: - acorn-node "^1.3.0" - dash-ast "^1.0.0" - get-assigned-identifiers "^1.2.0" - simple-concat "^1.0.0" - xtend "^4.0.1" - undefsafe@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" @@ -27756,7 +26965,7 @@ url-template@^2.0.8: resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21" integrity sha1-/FZaPMy/93MMd19WQflVV5FDnyE= -url@^0.11.0, url@~0.11.0: +url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= @@ -27869,23 +27078,6 @@ util@^0.11.0: dependencies: inherits "2.0.3" -util@~0.10.1: - version "0.10.4" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" - integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== - dependencies: - inherits "2.0.3" - -util@~0.12.0: - version "0.12.2" - resolved "https://registry.yarnpkg.com/util/-/util-0.12.2.tgz#54adb634c9e7c748707af2bf5a8c7ab640cbba2b" - integrity sha512-XE+MkWQvglYa+IOfBt5UFG93EmncEMP23UqpgDvVZVFBPxwmkK10QRp6pgU4xICPnWRf/t0zPv4noYSUq9gqUQ== - dependencies: - inherits "^2.0.3" - is-arguments "^1.0.4" - is-generator-function "^1.0.7" - safe-buffer "^5.1.2" - utila@^0.4.0, utila@~0.4: version "0.4.0" resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" @@ -27984,7 +27176,7 @@ variable-diff@1.1.0: chalk "^1.1.1" object-assign "^4.0.1" -vary@^1, vary@~1.1.2: +vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= @@ -28429,7 +27621,7 @@ vinyl@^2.0.0, vinyl@^2.1.0, vinyl@^2.2.0: remove-trailing-separator "^1.0.1" replace-ext "^1.0.0" -vm-browserify@^1.0.0, vm-browserify@^1.0.1: +vm-browserify@^1.0.1: version "1.1.2" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== @@ -28457,13 +27649,6 @@ w3c-xmlserializer@^2.0.0: dependencies: xml-name-validator "^3.0.0" -walk@^2.3.14: - version "2.3.14" - resolved "https://registry.yarnpkg.com/walk/-/walk-2.3.14.tgz#60ec8631cfd23276ae1e7363ce11d626452e1ef3" - integrity sha512-5skcWAUmySj6hkBdH6B6+3ddMjVQYH5Qy9QGbPmN8kVmLteXk+yVXg+yfk1nbX30EYakahLrr8iPcCxJQSCBeg== - dependencies: - foreachasync "^3.0.0" - walker@^1.0.7, walker@~1.0.5: version "1.0.7" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" @@ -28478,19 +27663,6 @@ warning@^4.0.2: dependencies: loose-envify "^1.0.0" -watchify@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/watchify/-/watchify-4.0.0.tgz#53b002d51e7b0eb640b851bb4de517a689973392" - integrity sha512-2Z04dxwoOeNxa11qzWumBTgSAohTC0+ScuY7XMenPnH+W2lhTcpEOJP4g2EIG/SWeLadPk47x++Yh+8BqPM/lA== - dependencies: - anymatch "^3.1.0" - browserify "^17.0.0" - chokidar "^3.4.0" - defined "^1.0.0" - outpipe "^1.1.0" - through2 "^4.0.2" - xtend "^4.0.2" - watchpack-chokidar2@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0" @@ -28873,13 +28045,6 @@ wide-align@^1.1.2, wide-align@^1.1.5: dependencies: string-width "^1.0.2 || 2 || 3 || 4" -widest-line@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc" - integrity sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA== - dependencies: - string-width "^2.1.1" - widest-line@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" @@ -28913,7 +28078,7 @@ winston-transport@^4.5.0: readable-stream "^3.6.0" triple-beam "^1.3.0" -winston@^3.0.0, winston@^3.3.3, winston@^3.8.1: +winston@^3.3.3, winston@^3.8.1: version "3.8.1" resolved "https://registry.yarnpkg.com/winston/-/winston-3.8.1.tgz#76f15b3478cde170b780234e0c4cf805c5a7fb57" integrity sha512-r+6YAiCR4uI3N8eQNOg8k3P3PqwAm20cLKlzVD9E66Ch39+LZC+VH1UKf9JemQj2B3QoUHfKD7Poewn0Pr3Y1w== @@ -28979,15 +28144,6 @@ wrap-ansi@^3.0.1: string-width "^2.1.1" strip-ansi "^4.0.0" -wrap-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-4.0.0.tgz#b3570d7c70156159a2d42be5cc942e957f7b1131" - integrity sha512-uMTsj9rDb0/7kk1PbcbCcwvHUxp60fGDB/NNXpVa0Q+ic/e7y5+BwTxKfQ33VYgDppSwi/FBzpetYzo8s6tfbg== - dependencies: - ansi-styles "^3.2.0" - string-width "^2.1.1" - strip-ansi "^4.0.0" - wrap-ansi@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"