diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9d53eb7795943..cb9690ca3dc8d 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 @@ -109,6 +110,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 @elastic/unified-observability # Infra Monitoring /x-pack/plugins/infra/ @elastic/infra-monitoring-ui diff --git a/api_docs/actions.mdx b/api_docs/actions.mdx index 7b77296a7abb9..e89fd0e7ed367 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-12 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 8497e2ae833d5..a6de0eddd53d6 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-12 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 28fd735f6e461..db20b98ed0f6a 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-12 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 5eecc7808e303..21eec3051632e 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,17 @@ "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 } ], "initialIsOpen": false diff --git a/api_docs/alerting.mdx b/api_docs/alerting.mdx index d331897a74bc2..d46ff5aa181b5 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-12 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 | +| 369 | 0 | 360 | 22 | ## Client diff --git a/api_docs/apm.devdocs.json b/api_docs/apm.devdocs.json index efaf84a5b304a..6c0d9f74ea580 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\", ", diff --git a/api_docs/apm.mdx b/api_docs/apm.mdx index 27c873e1cbc08..371251c3d2f6b 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-12 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 167582665bb6d..48441c7541f20 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-12 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 363cb213dfa2c..051acd0e75900 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-12 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 2910180b02cdf..a0952458e0f45 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'canvas'] --- import canvasObj from './canvas.devdocs.json'; diff --git a/api_docs/cases.mdx b/api_docs/cases.mdx index cbc8bc0dcfd44..dbd163dff3005 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-12 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 ca9fc19e1213b..94a6853c57cce 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-12 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 bffc54e20d6ab..67f888cd10fab 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-12 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 40ee284d2734c..2d93e7b6a7ef6 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-12 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 88390589d3835..885e264937e68 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'console'] --- import consoleObj from './console.devdocs.json'; diff --git a/api_docs/controls.mdx b/api_docs/controls.mdx index ae609d41255ab..07fbe27b7c048 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-12 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 35d2b408052f5..44e4755f2f47d 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" @@ -18603,6 +18631,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 +18647,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 +18989,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 +19057,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 +21179,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 +21433,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 +21451,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 +21466,7 @@ "ServiceStatus", "" ], - "path": "src/core/server/status/types.ts", + "path": "node_modules/@types/kbn__core-status-common/index.d.ts", "deprecated": false, "trackAdoption": false } @@ -36027,6 +36080,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" @@ -46420,7 +46481,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 +46497,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 +46510,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 +46526,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 +46542,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 +46558,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 +46967,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 +46986,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 +47008,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 +47028,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 +47045,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 +47068,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 +47087,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 +47103,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": [], @@ -52214,7 +52272,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 @@ -52405,7 +52463,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 1bd545035359a..9d6e782717324 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-12 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 daf36243e408d..604f24762a12b 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'customIntegrations'] --- import customIntegrationsObj from './custom_integrations.devdocs.json'; diff --git a/api_docs/dashboard.mdx b/api_docs/dashboard.mdx index d7da06c8bda6e..454aa521d5942 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-12 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 7e58cc9e1a86a..125f6303237f3 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboardEnhanced'] --- import dashboardEnhancedObj from './dashboard_enhanced.devdocs.json'; diff --git a/api_docs/data.mdx b/api_docs/data.mdx index f7f878ec5cf74..bb06789a9d338 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data'] --- import dataObj from './data.devdocs.json'; diff --git a/api_docs/data_query.mdx b/api_docs/data_query.mdx index 5206f1ec46730..e862a2588c40d 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.query'] --- import dataQueryObj from './data_query.devdocs.json'; diff --git a/api_docs/data_search.mdx b/api_docs/data_search.mdx index 948da55a39f74..247b776e53e30 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.search'] --- import dataSearchObj from './data_search.devdocs.json'; diff --git a/api_docs/data_view_editor.mdx b/api_docs/data_view_editor.mdx index 95fa4182594f8..594122e246d10 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewEditor'] --- import dataViewEditorObj from './data_view_editor.devdocs.json'; diff --git a/api_docs/data_view_field_editor.mdx b/api_docs/data_view_field_editor.mdx index 19de55351a8de..2881eb78d6fbd 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewFieldEditor'] --- import dataViewFieldEditorObj from './data_view_field_editor.devdocs.json'; diff --git a/api_docs/data_view_management.mdx b/api_docs/data_view_management.mdx index b1851139b7d9e..fe964392680cd 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewManagement'] --- import dataViewManagementObj from './data_view_management.devdocs.json'; diff --git a/api_docs/data_views.mdx b/api_docs/data_views.mdx index 892d584ba199e..35957ede2cc4f 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViews'] --- import dataViewsObj from './data_views.devdocs.json'; diff --git a/api_docs/data_visualizer.mdx b/api_docs/data_visualizer.mdx index 2dd8caad2c655..63c9c3051d4e4 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-12 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 df2ac0366eadf..d9ba036f7bbe6 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -74,7 +74,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-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 | diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx index 600739e4a9fde..07f70de66a166 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -70,6 +70,14 @@ so TS and code-reference navigation might not highlight them. | +## @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 | diff --git a/api_docs/deprecations_by_team.mdx b/api_docs/deprecations_by_team.mdx index 8545316f77c0f..4a9e44b189a79 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-12 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-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), [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), [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 | diff --git a/api_docs/dev_tools.mdx b/api_docs/dev_tools.mdx index 334a2c08e23c3..6ab2af56de8a6 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-12 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 6eb89f79f4807..c21e6fc7a14bd 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-12 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 3ddbd42c2bc73..844946af12dba 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discoverEnhanced'] --- import discoverEnhancedObj from './discover_enhanced.devdocs.json'; diff --git a/api_docs/embeddable.mdx b/api_docs/embeddable.mdx index 362609d3d6d49..6d0f630e84048 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-12 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 58bb7cbddd42b..cf1334edf5db3 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-12 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 002dd9eb27139..31c161302239c 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-12 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 5a45b7486d4c9..337602bc8deb6 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-12 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 b3f3fc87bda00..88767bde098f9 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-12 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 0161dd80da0d7..bfb01ed547536 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-12 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 8e717efd4ad23..491bb3f165423 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 3867ef968cdda..67fc800a393f5 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-12 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 f8a6757785b98..55e41dee84df0 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-12 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 9c151c832439f..272f14fa331c1 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-12 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 18e588eda0322..883e69752d7c2 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-12 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 7c7fcf1e51a85..2fd851509964a 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-12 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 227caffdd86ae..dd3e3a7c1b9c4 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-12 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 73127734fb39d..ec3bd2846e1a8 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-12 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 ee91044586ad4..323af21664b16 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-12 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 30188432c4589..ce8b70c802891 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-12 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 abbd5e0f52ab5..44d11e482c899 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-12 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 ae05b346c5294..170818628b752 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-12 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 3df0f71990ee1..ba84f73e3531a 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-12 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 312da9ec73cca..a1190ad13be1a 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-12 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 9720c8b749c92..e58dcecd23eb3 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionXY'] --- import expressionXYObj from './expression_x_y.devdocs.json'; diff --git a/api_docs/expressions.mdx b/api_docs/expressions.mdx index 3bc004877bf6d..be93903834285 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-12 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 d3bfbec1eafaa..7084d5dd8614b 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-12 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 47ac9e94ccb6f..a3a2ffc66d724 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-12 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 00ef1834df39b..5acb5094dcbe3 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-12 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 b94441baeff07..d2657f3258452 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'files'] --- import filesObj from './files.devdocs.json'; diff --git a/api_docs/fleet.mdx b/api_docs/fleet.mdx index 212a823589def..deb8f6162875b 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fleet'] --- import fleetObj from './fleet.devdocs.json'; diff --git a/api_docs/global_search.mdx b/api_docs/global_search.mdx index 151db33c2bbd0..3318c54d6b6f9 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-12 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 0633e4a824850..ad5da6eb52715 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-12 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 7cd6a257eeca1..a566a62d1f846 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-12 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 b83bcf59070e4..5285ccf531912 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-12 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 05616b0cfadc4..96c41a5268b4d 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-12 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 8454800519cb0..28ccaee3ae731 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-12 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 100b024c58ddf..e2601319591a4 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-12 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 4606d800cf22e..1e71960980198 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-12 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 765ff1f91971f..f55d67c1fc3dc 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-components'] --- import kbnAiopsComponentsObj from './kbn_aiops_components.devdocs.json'; diff --git a/api_docs/kbn_aiops_utils.mdx b/api_docs/kbn_aiops_utils.mdx index 4b51bc25be890..89439f8a35a2a 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-12 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 d5a04ff5eee17..132c55a0f5320 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-12 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 a99efc95e8c13..023dcb2b4bb36 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-12 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 1fd92e71b327a..e491c482cb629 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-12 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 4f9fac4a0b2ec..c4ba600af1dab 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-12 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 ada937d1aa30e..bb77b10ad5c55 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-12 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 360208b56403b..1aa6719c6e313 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-12 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 995d23de7fcc7..180b6a7699ad3 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-12 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 41246db0a6756..4b863396dbcf4 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-12 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.mdx b/api_docs/kbn_apm_synthtrace.mdx index 82c57310848d5..01a492a721e05 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-synthtrace'] --- import kbnApmSynthtraceObj from './kbn_apm_synthtrace.devdocs.json'; diff --git a/api_docs/kbn_apm_utils.mdx b/api_docs/kbn_apm_utils.mdx index 3b8859bf24577..14b01e3c7be4a 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-12 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 a84beaaf76f9c..a129e1b408e44 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-12 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 f81ea7eb893f4..dc390f25f0170 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-12 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 f136b1b4b9722..741443fe91e35 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-12 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 4fac55f5f28f2..40098db9d607f 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-12 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 9a986e49442eb..4d457a94dff83 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-12 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 ad805a672ec6a..75ad6cf16351d 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-12 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 d48d41a5c8649..988bba727b23c 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-12 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 1e6fbb0c52619..f308239b6d475 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-12 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 4ed5acbd873d2..0714ab8c48ebf 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-12 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 1a85935c87425..1cd0b626f5510 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-12 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 ac39a7961593a..7777a057c2894 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-12 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 fc5cd3bfb8438..b85fd73d4c901 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-12 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 4c9c75e852390..8937453ad90ef 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-12 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 92c1424297816..21d0c98603bd5 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-12 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 da3c85dc76a4e..5bf553a6b4e27 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-12 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 e291353f701f6..84e37926b9d2e 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-12 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 e1d48305d2290..08601a43abe02 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-12 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 1b208d9a63487..42e241da77343 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-12 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 3ad4f503057fa..5600e432ddde8 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-12 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 4779f2f4f68b5..36b74b926858b 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-12 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_base_browser_mocks.mdx b/api_docs/kbn_core_base_browser_mocks.mdx index 701892c07d6fb..53392790ff321 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-12 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 5a164327bf338..e5fae7dc02d76 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 6f65974f3df95..4596828c8a7f7 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-12 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 3ac31dbf66d23..8c56c35df8037 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-12 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 5d9da0822c070..e369bc6cf9573 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-12 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 0fb84b17ee969..a02516d41e635 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-12 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 24dc0372c51ec..4cf9fbbbd4890 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-12 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 a52d64e5e6e20..ed7cb4116ff68 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-12 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 5e2bfe3318f47..d6275075f4f2d 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-12 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 8e3c4efe990f0..8b8b3352b3e03 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-12 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 ecc9bc1bad7b3..26b3493760d23 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-12 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 fa73a6310dd90..6aeec78a9de0b 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-12 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 e9d543f5eab88..cb3d85753a695 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-12 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 5e380c589df45..3f5b097166b70 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-12 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 86d55e1e9c44d..c3308a653104f 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-12 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 6dd116c3c46f2..b77b28d64156f 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-12 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 96418b9c64b68..a4c4c5108d54e 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-12 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 b557d4e5c473a..6d190e7921922 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-12 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 0cacd094cfbb9..adcb25e4e3b65 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-12 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 2c72b3d00c599..a2fc67402f23e 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-12 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 1582e2b3c6a47..125a8ddb62eb2 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-12 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 7cad22cf1928b..490c09706c8aa 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-12 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 ac6c785eb5bbc..8507ca2cb3903 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-12 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 65546108902bd..9aa393700f1cc 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-12 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 b119bfb2ad808..64475b318c506 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-12 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 d8d8afa63313d..3b16e74a4edd2 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-12 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.mdx b/api_docs/kbn_core_elasticsearch_server_internal.mdx index 7bb38a3c5f0bf..19bafc1b112da 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-12 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 da30359337d2f..2afd793f95c19 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-12 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 115eabde42da7..7e71e344bbf24 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-12 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 edfcddc56edc1..36cab5deb51a2 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-12 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 6d3b3e2cf0095..1a6af787887d5 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-12 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 fcc2284968ba8..ca61b99d2dbab 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-12 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 48919a14adc5d..177fc4c26f4ae 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-12 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 0a87811f9fa6d..4200bb38465cc 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-12 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 be640b0bd6c12..b95726bae4f8c 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-12 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 4d9363ee6d067..96baa43f4f803 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-12 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 8e1632be18c1b..9d90489b193a2 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-12 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 7fee5984f817a..dc43d1ef8ebe2 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-12 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 c989c3c38eff4..72d95cc2173bb 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-12 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 ab44b913d6d22..2604bdf1307f6 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-12 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 a23bfab235572..a70c8ebdd2038 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-12 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 cc4ba299fcd2f..090d7dea7fc90 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-12 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 f3ad4235ca0bd..062468ef28b45 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-12 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 8e03645953472..13666c7455aa7 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-12 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 cd7e2beebf898..8f122bb2f056e 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-12 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 355ed41119b58..7bf289e5bff95 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-12 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 bca3d59fc66ba..d5afd2459cb6a 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-12 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.mdx b/api_docs/kbn_core_http_server_internal.mdx index 8c1ce95e5da38..643fc21c88e99 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-12 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 9855a467f8ff3..337c83d0c71ed 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-12 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 e10bab2bb0ef1..41b228a782728 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-12 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 6bbcf9a1ed58f..23037163144c5 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-12 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 ca0b28fad3b83..13949e3052dc5 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-12 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 d0b3e5f8750ae..35d1ab22608d6 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-12 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 09595678ffd95..0145a71c0d7b2 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-12 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 f98bc4b4caa51..87346ec4fa5b7 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-12 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 2d6f60b30b0f4..d9ba50274fda1 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-12 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 7d6d538a4a345..90e86fc55e65b 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-12 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 965b44c3d7f49..60f31535742b4 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-12 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 baf526633cb7f..302e26129c712 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-12 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 c7e3e793271dc..337f272c28fe5 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-12 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 ba256a3dc7685..8d4b519f59c14 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-12 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 20ddad2be7f37..196c31459d3f2 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-12 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 6278558d4a15b..730e06acc610b 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-12 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 774dbb4aa58f2..11c953906f0dc 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-12 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 b68f29089b3c5..a5ba2bcf8c3cc 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-12 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 9da56c2b13d54..75f3667db69e4 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-12 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 fc5ad5407ea58..ace513c8f3fd6 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-12 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 cbc248a380a6e..ee9a394184d4e 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-12 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 6c389f91fb1de..5333d41e14584 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-12 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 e8223e2b45977..4a89b92fff036 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-12 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 2c27b78aff6bb..14247d429667c 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-12 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 197b80b4ba630..0e8f65ff9da07 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-12 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 bf9b1d6b28607..52483dfa4dc6c 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-12 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 21561dfeb133d..857037bab1093 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-12 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 d4790272c355d..b2767486e6592 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-12 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 1e44984b977ae..6afb7d61f7327 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-12 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 064c1a7409c91..23ae70cfd0c9f 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-12 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 68d8ba8c77e35..7a3a3d003792f 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-12 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 33c45bca370db..23db97bef04a9 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-12 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 8fb776cf68240..f7574d68fb4aa 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-12 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.mdx b/api_docs/kbn_core_saved_objects_api_server.mdx index 37b831645698d..fb970d77d9a9b 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-12 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 b1e0f26591dea..71eb12db08bbc 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-12 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 76e6857265343..d84f5c5c73c7e 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-12 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 b0656d2d7698c..93ab71cd6d0c2 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-12 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 ecd8eb6b89e58..089cea7cbcd40 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-12 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 3095a10b0e8a3..ac8a36714f9c9 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-12 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 6e6235f54d936..afa0e5688428d 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-12 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 9ba194c06f4bd..fbecff8bedc44 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-12 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.mdx b/api_docs/kbn_core_saved_objects_common.mdx index 841d63be63be4..56315c20f6063 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-12 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 62ca0892a8ef0..ae5b78204d1b8 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-12 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 6db912ca3d51d..0b3f8c2f078bc 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-12 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 124ba03f2d7cb..e2f35211ba038 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-12 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 436795ec64440..c13d91d1c3d3b 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-12 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.mdx b/api_docs/kbn_core_saved_objects_server.mdx index d39b682d0e922..6e430a5fa2611 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-12 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 097b612967b60..06e6a2ecbe278 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-12 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 5cfc809e628d4..f6aecda23d6f1 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-12 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 0735565b60ba3..5fac3e3216d8a 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-12 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 0000000000000..b49607b86963e --- /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 0000000000000..1743916cfc825 --- /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-12 +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 0000000000000..39b4d63b9e25a --- /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 0000000000000..f815aa153ef38 --- /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-12 +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 0000000000000..6438e27aa69d3 --- /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 0000000000000..30a90ae7a976a --- /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-12 +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 0000000000000..d24cd2e143830 --- /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 0000000000000..1b1bcdac498dd --- /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-12 +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 0000000000000..85f331d9e7e27 --- /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 0000000000000..d2a4f74edd929 --- /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-12 +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 bcc58ce97c172..7fecf1916746c 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-12 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 fab15b3ed6f35..794d64d7e64e9 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-12 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 017403c279a71..f0d5725c883fa 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-12 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 4b18d3cb540b5..65edbdba1a628 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-12 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 d68eda65b7478..d9f583d515cfc 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-12 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 d43b64677389a..925ad46a92610 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-12 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 a2b638ea5c8f3..10165c0389dd6 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-12 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 6a7ad1ab8e14d..fed0b546122cd 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-12 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 9b16f81a2be4a..0732415264371 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-12 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 135387c16286c..5052cc57dc19e 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-12 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 cb5b74bd34c0b..79a90a07cb013 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-12 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 02f152900b447..60c5375cd230d 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-12 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 70e8d69a6b17c..34d1cab6fdcee 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-12 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 77788c27073dc..cc0de7274d49f 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-12 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 669752ea17991..59692fd76e326 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-12 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 653e1b5efe544..b29893225438d 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-12 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 f7edb115f49a9..a95ffcdf1e28b 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-12 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 f83c97ef5f49d..5daa6d13cf6a3 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-12 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 7278c02638113..e76afe4af60ff 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-utils'] --- import kbnDevUtilsObj from './kbn_dev_utils.devdocs.json'; diff --git a/api_docs/kbn_doc_links.mdx b/api_docs/kbn_doc_links.mdx index 044c765674ea1..0fde4b55f119e 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-12 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 d47c4a94ac534..2644e4c3b5e71 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-12 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 5cc4b3d097f4f..beefe799a31bf 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-12 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 5cad59a4b8f74..59951f466f831 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-12 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 ca0ada5848034..cce2805a4412c 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-12 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 792b606b4133a..db4ae2018ca5b 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-12 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 6e1eb7f3fcfde..28ee9c2cff91c 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-12 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 3fa74decb5caa..136a0dd3d55f0 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-12 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 5c3fcdaab984c..eb5455dd7d830 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-12 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 ed9435a3b3b74..40b7ac766e87a 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-12 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 3e663271b7a7d..57561b5cbc50f 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-12 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 89013d791f80d..d2c265790b9b4 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-12 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 03c70dae79e77..2bf014a652c69 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-12 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 4f6ed6ee570a8..c2c4c4d0337a8 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-12 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 0d51ff6ce1465..4049be82b4dfb 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-12 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 dda867b946f49..306996d8dcc16 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-12 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 1eeda889c5cdd..e7387cd969eff 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-12 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 8c87bc7a914bd..dbc89d69930c2 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-12 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 1f89b3b7e535a..943a7e8f6ee37 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-12 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 6885069422ac0..42d8da1b6627e 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-12 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 771d6d1341fca..dca53916d65b6 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-12 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.mdx b/api_docs/kbn_logging.mdx index eb7eb96ae58c2..6ba7fb075d16d 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-12 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 c778ebc86da32..2832f3e8dae4b 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-12 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 f9799e1ac3947..9860f16923015 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-12 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 fc653bed6e7e9..aff7a4c3270b2 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-12 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 12b0ecb60e66c..a162aa944dc67 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-agg-utils'] --- import kbnMlAggUtilsObj from './kbn_ml_agg_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_is_populated_object.mdx b/api_docs/kbn_ml_is_populated_object.mdx index 67f7ef712b419..875e36a1a9991 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-12 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 a8aa07a6cb0ad..9a84263750a9f 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-12 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 cf5344b7847eb..c7162db39b87c 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-12 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 59384f2ce9406..8b0c2dce899c4 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-12 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 cb5c6845f2d4f..eff3e884b3bd7 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-12 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 2445802229bf1..3fdf23cd53c02 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-12 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 617d5fec736bf..37bb135d5c280 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-12 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 44df77707e11d..a5383cbc3b530 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-12 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 6917f3fc99b3b..ba7590ffbe159 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-12 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 9b30733e7280b..017f82899a837 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-12 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 d278bd47a83cc..49f9e90702310 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-12 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 e1c8c47ea5d19..ebe0a2779fedf 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-12 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 d2a1be19c6c17..15d8fcc4196ee 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-12 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 e4fbf89a1405f..a3eb896fea4c6 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-12 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 38f823267cd07..db43ecbf5d190 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-12 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 adec0fb8b5efb..3fa0a82da7e87 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-12 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 cf020e8b0a27a..90ed9b37626b4 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-12 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 598a866af57e8..43dbe59c8b441 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-12 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 7f8c8c6110751..79e9918ff415a 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-12 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 3690bc8cbc2b4..4260459c9d064 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-12 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 e30499cbbf39a..d7da0dc87559b 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-12 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 22657a04b1bb9..93b46261b29ac 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-12 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 19a2c081a5bd7..73955118e002c 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-12 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 65f02eb6305e7..e0d4fb5f40c7d 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-12 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 f84326704b432..ce83347c99d76 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-12 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 2553be04e72c4..aaee03632d3bd 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-12 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 e1deb3f5d47f7..72c05e4c8e597 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-12 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 e28e8dfe7c35a..5396d1d529cc3 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-12 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 57ee3c5eaeeac..ebeb4c6b525a9 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-12 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 823e9843677a0..af5b4f23845c2 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-12 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 27455c0dcff20..6fbaa004f2aa1 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-12 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 30e9422c7d0dc..216b4ec105498 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-12 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 d382db6b06181..86b17203dd3aa 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-12 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 f719f64919ffe..ba2a281401304 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-12 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 6c98183d2a7ff..fc4bed4b1a001 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-12 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 e96fd6b65b797..d0c9c5ed9392b 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-12 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 f082939bf23db..f6aa153d98fd1 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-12 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 56d513e6b2395..967834391e6dc 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-12 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 f2aa470f834cc..527b5039281e5 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-12 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 003b096cfd0f9..b132757f0b152 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-12 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 8b61bc3b8fb03..92be11cf2c6b6 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-12 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 9b0e41366e492..579464a3146de 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-12 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 673f7aa80d418..c89f5a58f576e 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-12 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 fca609edd1eec..f7430ec4cffc9 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-12 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 d55f975104ffa..770abdeea3c3d 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-12 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 370bb3fb7b081..f20bc874aa489 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-12 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 0000000000000..b3381733e9f8f --- /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 0000000000000..c88c99fdfa8e1 --- /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-12 +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 0000000000000..db66a62441697 --- /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 0000000000000..ad038875cef83 --- /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-12 +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 9200b11a09b21..008ba2f53e93c 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-12 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 d281f952bbbdf..7883d6366d4f3 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-12 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 ce6cfca9bcf5c..c7fe9875c57c6 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-12 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 ef9f4272e5ae1..bc834ee2ff41a 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-12 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 5e5cd15a35874..1dd32b08c95c5 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-12 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 f09537bfe88ae..a76ec2f91fad0 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-12 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 9bd71879017ce..ffd690fb8d542 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-12 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 9a002696753d0..6d4cee2cf48e5 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-12 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 5b0eba5853c13..bb0e1cfdac9b0 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-12 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 3a4cf723d0775..f36daceb7cab2 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 2b1cfb4d5c2fe..bbfa64010e3ba 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-12 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 20c4a3eb4f9ba..762df9d42c29c 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-12 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 fb392e818e687..957ed8febfcae 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-12 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 c06226b8eba96..98103fbb8df50 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-12 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 b81adb3a56d1d..ff85afae240ef 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-12 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 e75d872f2484d..9e427901caff1 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-12 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 b70ee2231fefb..6900408be11c0 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-12 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 b7ddf660ca0df..cd577edea97f4 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-12 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 92cd97760ca7c..cbe25e33be509 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-12 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 af84fa702b075..e2a850694866b 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-12 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 c6f2cc8482954..e09f9577b2b03 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-12 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 789133a70ac7e..a789dd3553362 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-12 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 4ea296d9ab1e1..8af7864ea4939 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaOverview'] --- import kibanaOverviewObj from './kibana_overview.devdocs.json'; diff --git a/api_docs/kibana_react.mdx b/api_docs/kibana_react.mdx index d69d9e8545c96..cb45d6c5a4431 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-12 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 169dcb7ee1f23..0de39481f8629 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-12 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 7f3978dab8e03..1652626d9f56f 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kubernetesSecurity'] --- import kubernetesSecurityObj from './kubernetes_security.devdocs.json'; diff --git a/api_docs/lens.mdx b/api_docs/lens.mdx index a22c71323b68c..6e43b6db2c76b 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lens'] --- import lensObj from './lens.devdocs.json'; diff --git a/api_docs/license_api_guard.mdx b/api_docs/license_api_guard.mdx index 571dc8691bd0a..48955617613a9 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-12 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 201e1c21dffbc..8c8b8e8c9ef66 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-12 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 d00e7742b2ed4..1f36b3105cdc8 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-12 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 751f7ca0cd136..a726ebc34c5fb 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-12 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 9a598bdde11fb..74513f78d1f36 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'management'] --- import managementObj from './management.devdocs.json'; diff --git a/api_docs/maps.mdx b/api_docs/maps.mdx index fb06fd880fdb0..a5fb71bb58e91 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-12 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 ff860e62ea1b9..7cac92107a18c 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-12 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 ed642dcfa50a6..616e1e5a07fc7 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-12 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 ddf5e67004844..afe72ce3daf47 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoring'] --- import monitoringObj from './monitoring.devdocs.json'; diff --git a/api_docs/monitoring_collection.mdx b/api_docs/monitoring_collection.mdx index 6fb7aa53be7c7..c2674d018305b 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-12 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 e92a9aab1928c..ac8837b6ca894 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-12 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 ef7c19dc1b7fe..ba58d7518c6e4 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-12 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 680c70e75c060..b37b1655b9065 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 7a1e6c0e0601f..c580985bc8887 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-12 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 f217e15c771ef..8d45fc77730ef 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 8398670e49a38..1e938d796a4a7 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-12 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 f6762a00b3cc2..40d73b16b018a 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-12 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 | +| 450 | 375 | 36 | ### Public API health stats | API Count | Any Count | Missing comments | Missing exports | |--------------|----------|-----------------|--------| -| 30641 | 180 | 20479 | 966 | +| 30722 | 180 | 20534 | 971 | ## 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) | - | 369 | 0 | 360 | 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,7 +41,7 @@ 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 | @@ -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 | @@ -114,7 +114,7 @@ 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 | @@ -212,7 +212,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | Kibana Core | - | 12 | 0 | 12 | 0 | | | Kibana Core | - | 8 | 0 | 1 | 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 +307,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 | @@ -398,6 +403,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 +414,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 bd88e5f88c6ff..ecf895a3e0f7a 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-12 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 4dc8865027be9..9d61b29944ba9 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-12 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 98619fec51a28..286139d6fe63f 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-12 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 32c76e983e404..3a26281e749b6 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'rollup'] --- import rollupObj from './rollup.devdocs.json'; diff --git a/api_docs/rule_registry.mdx b/api_docs/rule_registry.mdx index 0c20752827038..c9236042b14f0 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ruleRegistry'] --- import ruleRegistryObj from './rule_registry.devdocs.json'; diff --git a/api_docs/runtime_fields.mdx b/api_docs/runtime_fields.mdx index 3da2926bef45d..d04909af54896 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'runtimeFields'] --- import runtimeFieldsObj from './runtime_fields.devdocs.json'; diff --git a/api_docs/saved_objects.mdx b/api_docs/saved_objects.mdx index ccb2436bc7ab5..03f4861574ec8 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-12 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 8ebb9e956a9bd..312293e55cc62 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsFinder'] --- import savedObjectsFinderObj from './saved_objects_finder.devdocs.json'; diff --git a/api_docs/saved_objects_management.mdx b/api_docs/saved_objects_management.mdx index 96fc861f282ec..e629a877e6a51 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-12 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 f1f07a53b63c0..456808d926a82 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-12 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 10469dabcc739..8dc7f9e54649e 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-12 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 e0fd13863fddb..15f259f2794d4 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-12 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 ab9308881edad..5c308f8c268e9 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-12 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 25835a28fb913..47f80ea4bfb32 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-12 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 7aeaddd49f9fc..e1d55dd7b8f5e 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-12 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 f2e9de3b4461d..29a07610f1e5c 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-12 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 a0b46b34aff8c..9c8e4190bf491 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-12 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 917ac7b5c8b7a..6bd52414173b9 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-12 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 3d2d4e9f7d91b..1e59aff6842ba 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-12 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 039252a8b5f89..02b72e1835c09 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-12 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 e1bbb20979c05..85ade27d7ba7e 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackAlerts'] --- import stackAlertsObj from './stack_alerts.devdocs.json'; diff --git a/api_docs/task_manager.mdx b/api_docs/task_manager.mdx index 6f5a5d420ec86..696dc5d871335 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'taskManager'] --- import taskManagerObj from './task_manager.devdocs.json'; diff --git a/api_docs/telemetry.mdx b/api_docs/telemetry.mdx index fe725f5cd654d..3a62678f11769 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-12 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 c510c72159d82..30f763ba4488f 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-12 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 79bcdf8736b5d..f56b57fe0398c 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-12 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 a79d78af7bd71..80abde5c72bc8 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-12 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 5a830485f55ec..8e620fcd48321 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'threatIntelligence'] --- import threatIntelligenceObj from './threat_intelligence.devdocs.json'; diff --git a/api_docs/timelines.mdx b/api_docs/timelines.mdx index e8be0345a839f..020d59da436aa 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-12 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 d53e2f51177d7..27facd7cbc791 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'transform'] --- import transformObj from './transform.devdocs.json'; diff --git a/api_docs/triggers_actions_ui.mdx b/api_docs/triggers_actions_ui.mdx index 97a09460258c2..6b6af45f6d599 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-12 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 e95f3879bfb83..68f0239219c7a 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-12 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 477dcd64c4e87..f9e3c5956fea5 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-12 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 f2fbbc473c25c..82043a2029661 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedFieldList'] --- import unifiedFieldListObj from './unified_field_list.devdocs.json'; diff --git a/api_docs/unified_search.mdx b/api_docs/unified_search.mdx index c14315f59ce5a..5b3a5638c74ef 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch'] --- import unifiedSearchObj from './unified_search.devdocs.json'; diff --git a/api_docs/unified_search_autocomplete.mdx b/api_docs/unified_search_autocomplete.mdx index 74c9ed470c1f1..bebe8e757136a 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch.autocomplete'] --- import unifiedSearchAutocompleteObj from './unified_search_autocomplete.devdocs.json'; diff --git a/api_docs/url_forwarding.mdx b/api_docs/url_forwarding.mdx index b9c921a284d1d..ba4d70200d714 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'urlForwarding'] --- import urlForwardingObj from './url_forwarding.devdocs.json'; diff --git a/api_docs/usage_collection.mdx b/api_docs/usage_collection.mdx index 4de9f1cf325bf..eac1395483f26 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-12 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 d497f6bb0a446..ac07b7b6dec90 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-12 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 1237c1960ae3b..27386a94c37aa 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-12 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 54d7da6ff41a9..c25eb47e0fbdc 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-12 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 48c4485641f9b..492e3f940e990 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-12 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 f03b8bea14f0b..bd06921b3a740 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-12 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 db63128f3733e..32a53d10810cc 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-12 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 3767eed28f79c..1a0fe74391504 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-12 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 eaa8be6fe9eaa..faaeaad6e7c4b 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-12 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 56642da0d7bb1..9892ae3945683 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-12 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 20e284f805f36..d2862d4828d21 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-12 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 d7a9c73ca0265..8c22d419a997c 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-12 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 d708564591c58..eb856d91fcbba 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-12 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visualizations'] --- import visualizationsObj from './visualizations.devdocs.json'; 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 6f838631591ce..a68f09cba6a54 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 ad3b3c94ac6da..05eb4966496a0 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/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 521d7aff8f976..50dce25679252 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 63eca247cca6f..51cd024f0b53e 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 5dd0045ab5d68..8659e12909763 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 9979e96261e7b..4f7cc3e57a975 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 9a960674e061e..fc09b860f705a 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 0000000000000..49761aa122844 --- /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 e262d3ecbfe45..b8bf2673ac3bd 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 0000000000000..b9db87b65e3cd --- /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 47b871196be03..88c91ab645776 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 d90d7ef6cdf68..9518ba6cc89ed 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 a7cd508401c6c..dd66369a37d3f 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 36428579a30e8..d4eb463e67051 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 8d49702b48154..391f54581f258 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 0000000000000..d8a836ea583a3 --- /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 5b2e66c66fe39..bad8554100790 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/src/plugins/data_view_field_editor/public/components/field_editor/types.ts b/src/plugins/data_view_field_editor/public/components/field_editor/types.ts new file mode 100644 index 0000000000000..3c8aba9149ea1 --- /dev/null +++ b/src/plugins/data_view_field_editor/public/components/field_editor/types.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 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 { EuiComboBoxOptionOption } from '@elastic/eui'; + +import { RuntimeType } from '../../shared_imports'; + +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 6cadc094bb35f..494d6034f6669 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 9968838919513..edd0a7bc70b62 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 6696cf1e48b55..34740187d77d9 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 4211047878cca..7058b04b09053 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 0c23c8de616cf..2ae6b3149dc5f 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 05339e6473b6b..f331e62bd7016 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 127badffc826d..6d6c38f8dfc61 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 761c1db2094da..881d512159516 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 0000000000000..0a7a8a3990cdc --- /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 7f76210c2c6f0..95ec7e4dcfdc4 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 82051eef17663..6cf4381089226 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 2703653138fe0..b2c9bba61b98f 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 d938ae52642b7..6d3dbc6c3f4cd 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 6d7c6b4222d50..ff6621d64cbd1 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 8ee47f20515a1..3688f79c16689 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 6423694f76ec9..bee5fc0dbd1be 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 3c9eeaa63109d..208871f77022a 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 c054b42f51ac7..e06bcf99e3399 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 1646ec0f24ed5..ab6e82b301f01 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 4cc5792ce756b..01ffc9377d8b2 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 e4326439e574a..2a06f0c88b54f 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 a4b87110521d7..075869ab6fdc2 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 7c5e9314a153e..892df9b312e86 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 31a6b573ff2b3..a5d0447f79339 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 ffeeb069d1912..5f7b3a544db9d 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 ec0aeb081124e..54c8c61635f63 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 cf48aaee81fd0..f886d60696b8a 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/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx index f76343156c955..00cbd0a2ffcb0 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 d67e12cf8eccc..9f29f3ba7f69f 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 = ({ { 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/functional/apps/context/_filters.ts b/test/functional/apps/context/_filters.ts index 8c77d4fd013c1..f9e95080c92e4 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/discover/embeddable/_saved_search_embeddable.ts b/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts index 08a0296ad8c08..bd47c072e7735 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 6ea883f7a560d..2041d5fe500fc 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 c2f55847e7d1e..a90932595d42a 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 0000000000000..47ea33e443d22 --- /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 ded50870d7956..9b905b5a01074 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 013c0473a59b9..b71458c5c5527 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 ab47fb31fea54..b0f2efea40993 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 da12279a3ffa1..ec2d5385a0a43 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 b7b8c67a4280d..54c9e5a1ee54c 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 a240e8031cd68..4a190266458ec 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 fbd4310489fef..68b2553478df7 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 31d31e6424177..e2186ddefa5fa 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/x-pack/packages/ml/aiops_utils/src/stream_factory.test.ts b/x-pack/packages/ml/aiops_utils/src/stream_factory.test.ts index a0c5212244ad6..1e6d7b40b22d0 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 9df9702eb0870..7d685369e4d10 100644 --- a/x-pack/packages/ml/aiops_utils/src/stream_factory.ts +++ b/x-pack/packages/ml/aiops_utils/src/stream_factory.ts @@ -106,13 +106,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/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index f0cf88615047d..7d0f6463a4872 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, @@ -2138,7 +2138,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 +2165,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 +2302,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 +2794,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 +2811,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 b29d41f183b73..f5192bf6cbe65 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 a193733aff26f..499f1c2e8454d 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 8923031ab6b87..b8d259bd6a682 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_conflict_retries.test.ts b/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts index 2416e95c38f64..b71cc0276dc64 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/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 75cd1f9cb9f94..0000000000000 --- 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 2696ad60a4568..63280d230b777 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 9e85d6e4cbf7a..a0488094283d0 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 9fef8ae47b3b0..2ae40c2e33961 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 add4c1c206dd4..c607eb2985af8 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 0000000000000..1166dbed8ca88 --- /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 dceb8fd0f30a7..a180fb942bd15 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 c764df4d6661d..e8661387e1e64 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 5e88831144b6b..7d445a4edffac 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 d8aedbc10bd3d..c38408c8f5417 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: [], 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 3c056ccf996dd..ce38c82f08384 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,12 @@ 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'; const ProgressLoader = styled(EuiProgress)` ${({ $isShow }: { $isShow: boolean }) => @@ -78,6 +85,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 +96,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 +146,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 +157,7 @@ export const AllCasesList = React.memo( filterRefetch.current(); } }, - [deselectCases, doRefresh, refetchCases] + [deselectCases, doRefresh, queryClient, refetchCases] ); const tableOnChangeCallback = useCallback( @@ -193,6 +223,8 @@ export const AllCasesList = React.memo( const columns = useCasesColumns({ filterStatus: filterOptions.status ?? StatusAll, + userProfiles: userProfiles ?? new Map(), + currentUserProfile, handleIsLoading, refreshCases, isSelectorView, @@ -245,6 +277,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 +288,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 0000000000000..6ffa18cfa8073 --- /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 0929f8971cf06..be5b8ace2e2b6 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 8e5263a31ff3d..38f0b9d53b1d1 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 4c477e1b4a581..23a8772852e17 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', () => { 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 cedd7c9b64718..25da22f9be168 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 [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 8715af5d6fa68..9e593fa6c282a 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,7 +19,7 @@ type Capability = Omit; **/ export const useAvailableCasesOwners = ( - capabilities: Capability[] = ['create', 'read', 'update', 'delete', 'push'] + capabilities: Capability[] = allCasePermissions ): string[] => { const { capabilities: kibanaCapabilities } = useKibana().services.application; 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 284785b33d720..3855f14134f46 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 0000000000000..fdd4b5cb77e7d --- /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 0000000000000..520c76fef5124 --- /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 db5411525290b..74ff651a0a11c 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 1e935c15891f4..647763d5461d1 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 c85f989f88281..0dd651f6c9251 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 0000000000000..d7b61e16b36b6 --- /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 0000000000000..b9ee1dd871b8e --- /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 0000000000000..27115acf5697f --- /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 0000000000000..cb824adf0a217 --- /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 70f9e7d2fbdfc..ab3b0f90c65a6 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 33620c91d87a2..9180244b9cf45 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 0000000000000..2ff593651abf7 --- /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 0000000000000..be4efc0edfed9 --- /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 78f5a4e9d5c54..d34e216c34af9 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 6d8520e61a493..8b45258889e5c 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 c455ae0c3628d..ee2d2d9a4468c 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 8f67b3c05d3e4..c54e3206b2b01 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 d72b1cc523f0d..59cf8f919606b 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 7e0f7e5a6b9d5..780a1bbd1d02f 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 3d7be7f08084d..5db400203468a 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/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 73f466aeec771..0000000000000 --- 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 7e6d9e2b05d94..0000000000000 --- 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 f36996c013471..e2893cbbc5aa8 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 707cb9b7c4335..c5c7ddcaab875 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 ec8a84de1aa88..dd27a4a91ca12 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 d31c297d18b1c..d9ba8890aab31 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 0000000000000..57cde43c9fee6 --- /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 42580ede0b3f2..e0a499df05633 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 66dbf74e6815b..f8dba47578d72 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 ef5b4418d454f..e04738a962686 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 10b676be711b7..f2b5987d8cb1d 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 80e4d0d3743ce..2d5cfa91e1500 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 6d07d9111fc8a..446cb6114e97e 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 e23a4efa2f0a2..b824f6f20276f 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 60fc0e92d024b..0991156cd3d4d 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 4a6bc85c7cbd7..1517450c4f62e 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 b3a7909b06929..b963947a6282d 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 2f52656c9fca9..01a4605f1651f 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 13273346241e5..132b824109e51 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 d5553a3f6f13d..f9d0203a5647f 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 b5b5d902d3a4d..91425c368286d 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 8ba409468851e..7477e6df8d5dc 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 d7db17bd97cbd..23d952738aa4d 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 0000000000000..fec173ac70c61 --- /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 0000000000000..4abd9f276abaa --- /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 0000000000000..3c0c935d9316e --- /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_list/translations.ts b/x-pack/plugins/cases/public/components/user_profiles/empty_message.tsx similarity index 52% rename from x-pack/plugins/cases/public/components/user_list/translations.ts rename to x-pack/plugins/cases/public/components/user_profiles/empty_message.tsx index 73610e5959345..a2c713d5a9abc 100644 --- a/x-pack/plugins/cases/public/components/user_list/translations.ts +++ b/x-pack/plugins/cases/public/components/user_profiles/empty_message.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; +import React from 'react'; -export const SEND_EMAIL_ARIA = (user: string) => - i18n.translate('xpack.cases.caseView.sendEmalLinkAria', { - values: { user }, - defaultMessage: 'click to send an email to {user}', - }); +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 0000000000000..3471aad3fec3c --- /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 0000000000000..638d705fade86 --- /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 0000000000000..b9611bb683d44 --- /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 0000000000000..87839fb7c3482 --- /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 0000000000000..d2f64a05e7ce1 --- /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 0000000000000..e1e8018a21e35 --- /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 0000000000000..beded4faf714b --- /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/plugins/cases/public/components/user_profiles/types.ts b/x-pack/plugins/cases/public/components/user_profiles/types.ts new file mode 100644 index 0000000000000..f4acb29809d68 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_profiles/types.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. + */ + +import { UserProfileWithAvatar } from '@kbn/user-profile-components'; + +export interface Assignee { + uid: string; + profile?: UserProfileWithAvatar; +} + +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 0000000000000..b98eef9efbd9f --- /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 0000000000000..1337239bf2dcc --- /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 0000000000000..be6a8ddfc9359 --- /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 0000000000000..5bda7ef8d3cba --- /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 0000000000000..8ca7fdd435fc4 --- /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 0000000000000..17d26a39c48f6 --- /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 0000000000000..9c837997b9a1f --- /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 f781daac15697..2b43135123080 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 45cde4c4f94cb..e51224dc593dc 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 70b9c4033a424..2b7e8910fb9da 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 3a04b411cb8e7..a87d773303447 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 92e601dd0c9e9..812349e96fce7 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 c5dbf017da8c9..a9d80181b58f7 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 da695201d6d76..1d36521d0b6f4 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 ce19e68fa1798..7b046cac3f13f 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 6601a104d9f7d..a8747a2bd43a5 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 38d47d3aa9cbb..0000000000000 --- 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 ce8aa4b961c23..0000000000000 --- 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 36c88451124ca..6901852a405fa 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 e9382f7092ae0..1296cf9878827 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 7234cc9fb54fe..0f7c9d9c31fa9 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 6da84d1991423..cfd1c04d0afbc 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 0000000000000..db4527ae31e43 --- /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 0000000000000..2e1bb0a61dbda --- /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 7591bf394d5c1..af0482f41b25a 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 78c310462f77e..de180b5970f3b 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 0000000000000..ebc896a480cb0 --- /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 0000000000000..37c29fa0b2d01 --- /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 ef5fe32a23dff..2d4482b94a9c6 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 6c83f853b2624..26e03d0163c8e 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 0000000000000..66e63e6950dd4 --- /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 827535d484588..75e15f8859e58 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 d2ddc6a1030a0..9f92c7d9398a6 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 36bc0c439a79e..4ba9eb630e53f 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 2de87bdb660f7..7cf3fb779942c 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 2af55809f1c91..05be275af41c0 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 6c5701a6593ee..39db06473c9a9 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/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 0ff9800716cfe..6d49e2e53cfba 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 a6d3455f91897..f76fcf41fa245 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/utils/create_pipeline_definitions.test.ts b/x-pack/plugins/enterprise_search/server/utils/create_pipeline_definitions.test.ts index 27208dbaed00e..6961086edac1b 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 377f12fd63208..666588dd09886 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/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts index b415328a303d3..fa9e074899163 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/routes.ts b/x-pack/plugins/fleet/common/services/routes.ts index 323d7d1f8b378..c2f76758c3d7b 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 9924413cb16bf..7b92e6e779fd5 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 { @@ -104,6 +105,22 @@ export interface CurrentUpgrade { startTime?: string; } +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 interface FleetServerAgentComponentUnit { id: string; type: 'input' | 'output'; @@ -122,8 +139,6 @@ interface FleetServerAgentComponent { units: FleetServerAgentComponentUnit[]; } -// Initially generated from FleetServer schema.json - /** * An Elastic Agent that has enrolled into Fleet */ @@ -309,5 +324,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 8c8e6288d474e..519222b4e3c10 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -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 ec6b04ae64fc8..4673fefbd9536 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 { ExperimentalDataStreamFeature } from './epm'; + export interface PackagePolicyPackage { name: string; title: string; version: string; + experimental_data_stream_features?: ExperimentalDataStreamFeature[]; } export interface PackagePolicyConfigRecordEntry { 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 dae156c8c8354..7050fcd3da346 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/kibana.json b/x-pack/plugins/fleet/kibana.json index 462e68a097962..6ab87283e0b26 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/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 b34ce95297761..1d0cda70edea4 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 1662f6d0c7214..166078eb01dd2 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, @@ -48,6 +51,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 +61,7 @@ export const PackagePolicyInputStreamConfig: React.FunctionComponent<{ packageInputStream, packageInfo, packagePolicyInputStream, + updatePackagePolicy, updatePackagePolicyInputStream, inputStreamValidationResults, forceShowErrors, @@ -111,150 +116,226 @@ 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; + <> + <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, + 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}> - <EuiButtonEmpty - size="xs" - iconType={isShowingAdvanced ? 'arrowDown' : 'arrowRight'} - onClick={() => setIsShowingAdvanced(!isShowingAdvanced)} - flush="left" - > - <FormattedMessage - id="xpack.fleet.createPackagePolicy.stepConfigure.toggleAdvancedOptionsButtonText" - defaultMessage="Advanced options" - /> - </EuiButtonEmpty> - </EuiFlexItem> - {!isShowingAdvanced && hasErrors && advancedVarsWithErrorsCount ? ( + }); + }} + 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 57b5376c9fbb7..541f54b792c7b 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/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 aee476723f1a9..cae8b00fb6d3d 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 72b1e00c1ed02..7c0c0136c3d09 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 07b284058fc7d..c60536961e01f 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/hooks/use_request/agents.ts b/x-pack/plugins/fleet/public/hooks/use_request/agents.ts index da082a1ff0d45..13f687e321e54 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/types/index.ts b/x-pack/plugins/fleet/public/types/index.ts index 72755e74d858f..2438272503ac7 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/scripts/create_agents/create_agents.ts b/x-pack/plugins/fleet/scripts/create_agents/create_agents.ts new file mode 100644 index 0000000000000..31df24c70a5d4 --- /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 0000000000000..62614b67ea69b --- /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/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index a76988506cad2..f3f9e5b59d3c4 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 0cec7c92c62b1..57e65664d5969 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 61d86c983fc6e..e64af66460ef4 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 d6277cd4983fc..b7d1a4907d8e0 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 7988a9883114e..b551cd8d39bf4 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 b6e398d269a6f..8cc4a7b13e687 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 d75e3ad07d9b8..ff78da6e63d1a 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 bd031cf9b0274..55d38b00dec3d 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/agents/action_runner.ts b/x-pack/plugins/fleet/server/services/agents/action_runner.ts new file mode 100644 index 0000000000000..634bf27ba23ef --- /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 0000000000000..bfda349ac3a05 --- /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 f0e2d059a98a8..a2ba066db7d3f 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 0000000000000..e80db905d48e0 --- /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 dbc2017c95cf5..c6d63b35ac7be 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 5b6e39c153b89..193bc71d04d29 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 0000000000000..229074acde82b --- /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 7712c614adbea..302790cf6ae6d 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 1746d324d626f..9889bc8a6eada 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 0000000000000..c1bdb0e467c0d --- /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 9ad39990b9ffa..668ab79da691f 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 d8a5b4b32a101..941e1260894b4 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 0000000000000..6eda4b00499e1 --- /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 748f85db2843b..7ac8e4e3f9e78 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 c4893c7769651..4496e16cbc476 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 0000000000000..906566aee9f41 --- /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 1083e8f728ee1..b6c50a3b5dc3c 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 0000000000000..ca2bd6a996d67 --- /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 86f9408bef3f3..3257f3e969ec8 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/epm/packages/update.ts b/x-pack/plugins/fleet/server/services/epm/packages/update.ts index 4ab47954f51ca..224c9332fad62 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/experimental_datastream_features.test.ts b/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.test.ts new file mode 100644 index 0000000000000..8c7afa5d30fed --- /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 0000000000000..2b8b05aed89c3 --- /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 e79ef47502504..d0d4fa4aae825 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.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 3d481db8d63dc..134abb86b6893 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -32,7 +32,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 +51,8 @@ import type { UpgradePackagePolicyDryRunResponseItem, RegistryDataStream, PackagePolicyPackage, + Installation, + ExperimentalDataStreamFeature, } from '../../common/types'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../constants'; import { @@ -80,6 +87,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 +168,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({ @@ -279,11 +291,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 +477,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, @@ -569,15 +604,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 +631,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 +648,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 +710,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 +727,7 @@ class PackagePolicyService implements PackagePolicyServiceInterface { currentPackagePolicy, result, packageInfo, + experimentalDataStreamFeatures, options ); } catch (error) { @@ -694,6 +749,7 @@ class PackagePolicyService implements PackagePolicyServiceInterface { packagePolicy: PackagePolicy, result: UpgradePackagePolicyResponse, packageInfo: PackageInfo, + experimentalDataStreamFeatures: ExperimentalDataStreamFeature[], options?: { user?: AuthenticatedUser } ) { const updatePackagePolicy = updatePackageInputs( @@ -723,6 +779,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 +802,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 +834,8 @@ class PackagePolicyService implements PackagePolicyServiceInterface { package: { ...packagePolicy.package!, version: packageInfo.version, + experimental_data_stream_features: + packagePolicy.package?.experimental_data_stream_features, }, }, packageInfo, @@ -873,6 +943,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, @@ -1379,7 +1453,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 be923a9fdaa6f..fd0cee334cc50 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 5220f18380dd1..bddce3acbc847 100644 --- a/x-pack/plugins/fleet/server/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/package_policy.ts @@ -87,6 +87,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 +121,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/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx index 863bc82485b81..9eb79190d44e7 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 0e4c0594db3c4..fb7d7646871c7 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 1c68844079fa6..059170d9702d8 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 930864091c2eb..63696a3a2196c 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/shared_components/filter_query_input.tsx b/x-pack/plugins/lens/public/shared_components/filter_query_input.tsx new file mode 100644 index 0000000000000..db585f5f28204 --- /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 924f678c1f96b..3f30eb64ff2c9 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/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 778a1a13e200e..5d68e29a88d08 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 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 c9cd0bbec7d35..69cd398c562bf 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/ml/public/application/util/dependency_cache.ts b/x-pack/plugins/ml/public/application/util/dependency_cache.ts index 3680f8b63b0c9..0b78fb460ed85 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/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 9d084708e6529..3037d84180349 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/observability/public/components/app/section/index.tsx b/x-pack/plugins/observability/public/components/app/section/index.tsx index dae85d07685ae..149e386085d20 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/pages/alerts/containers/alerts_page/alerts_page.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx index 6ec620f535db7..974164265b50b 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 8630c7850298b..83b059b04f5e3 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 b9036ea4320af..cca2bb765e719 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 5a1dfdaa30252..1f09cfc38cf4c 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/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 f52b61794ce51..fcadce3f18b19 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 ad303966d046b..4bbacaa7bb1ad 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 a6acbb0d2e40e..28e2942c1f606 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/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 4a1f91719bca6..1d9d3cbf455f1 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: { 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 3ed54c451f38b..96afe9deb98e2 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 e3516f982cc0b..2938251e177be 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 40eb009a71bd1..7a67c6fdeb65b 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 9b8d012e7b084..ddea34a936178 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 eedaf8ac5ce0f..a00f1b7a80d37 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 8f3d936ab362c..fee2466a49430 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 0000000000000..a8b369b86342f --- /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 5e158c51c02d1..ff464e7782bb7 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 15c6fa645de11..bc039b334a910 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 927d408884d20..ba56cfa0da62d 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 0000000000000..7b6949696bbee --- /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 69c4befec1b6c..c19dd10802f32 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/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 3c3edf4e988ec..59db45aff0fa2 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 348dfebed1fcd..31731cecbeccb 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 0000000000000..074c3f60006c8 --- /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 b750b37aa51b5..3fad5f9309532 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 0000000000000..49e559c9f4c24 --- /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 0000000000000..6b2d59c824ab3 --- /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 638fb4e432412..a693de9b2fa4c 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 2ed80cb02c0d3..fdd0b1c931bd1 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 6a7d5b849c771..f466a7f8cf495 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/security_solution/cypress/integration/cases/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts index 0b53557dbcd03..81ae0d5b40fd7 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.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/screens/all_cases.ts b/x-pack/plugins/security_solution/cypress/screens/all_cases.ts index 3f5bcb912ee44..8109bc31e07ad 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/upgrade_integration/threat_hunting/cases/import_case.spec.ts b/x-pack/plugins/security_solution/cypress/upgrade_integration/threat_hunting/cases/import_case.spec.ts index 13afc4c7889eb..4eecc36fe928a 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_integration/threat_hunting/cases/import_case.spec.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/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 ce1e1939ef1bb..5bffe92e6d019 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 4e8358f3810fb..8226175786ae0 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/investigation_guide_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx index 5148dde4d6b59..4e9ff49a2b1dd 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 c7f8481c36247..494ecb0c6b4d0 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 0000000000000..7b96f3886159c --- /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 0000000000000..3517bbf7643d3 --- /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 0000000000000..fe7b811bd70fd --- /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 0000000000000..32305a5916c04 --- /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 0000000000000..77e2f14c51420 --- /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/detections/components/osquery/osquery_flyout.tsx b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx index 126f057742901..4999d757cd047 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/timelines/components/fields_browser/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx index 96ed68d32398d..0f7a5e0625216 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 d15fd7a501ea0..9fb4f2b13adb7 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 @@ -77,35 +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; } 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 b34338b4cbce9..065ac297ee468 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 83e06db651e00..00397ea43e59a 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/server/features.ts b/x-pack/plugins/security_solution/server/features.ts index c962e9fbf2d87..2a794285b52b7 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/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 d8c8e4394f3a5..9e295496dd77c 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 12783d39e2711..8da37518fbede 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/task_manager/README.md b/x-pack/plugins/task_manager/README.md index 4a99889ad9cc3..59f836c10c03e 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 0b01d6a05b7e1..4f1d41cad2109 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 1560c20be5baa..9ce4797e50db9 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 2bb06b3a223be..08b7a2908f2e8 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 086a435a39fa9..4b04672615b7d 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 afdd9c5c18b33..d477c9c643a49 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 67175c86370d7..a23c29a5044f3 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 f15639661e5d0..7226a55854988 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 d046a9266cce5..00129ac1bcdd4 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 a59fb077dbdeb..fe8cc3f81eced 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 eb4ff6c1b0ecf..a147a6bdabc6b 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 6d12a3f5984ca..ebb957e54699a 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 f61bd762de458..a9c58b1302f56 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 0b735d6b0ede6..a5865abc46bbe 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 15c8dc06c473a..08f36661dde52 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 a94394527a61d..071b0147e19b2 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 9434ba4fba0c3..8cd3330052cf4 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 8c9c690924218..945ab9b85a4f1 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/scripts/generate_indicators.js b/x-pack/plugins/threat_intelligence/scripts/generate_indicators.js new file mode 100644 index 0000000000000..bade9615b630d --- /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 = 'ti-logs'; + +/** 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 = Date.now() - RANDOM_OFFSET_WITHIN_ONE_MONTH; + + 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/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 3e1a916085b40..a7aadfb61248a 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -31665,7 +31665,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 6c875ca0c2333..fd4a320e417e1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -31642,7 +31642,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 05d1f7e1f51e8..8002605d93758 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -31675,7 +31675,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 0000000000000..293c1e992ac18 --- /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 aeba9605cb9a6..c938b0b2cc13f 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 0000000000000..b0e5416bbf63d --- /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/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 0000000000000..39264020f30a3 --- /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 6fc657d051594..708b12ceb7b4a 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/test/alerting_api_integration/common/lib/task_manager_utils.ts b/x-pack/test/alerting_api_integration/common/lib/task_manager_utils.ts index d8b1397edbd75..19d62bdd0682a 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 e601c6ee15ec7..f775b3607fade 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 842a00366945a..2f452a54927b9 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 0aba468174cff..73842073a542b 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 19c10bddbd7b1..c20e41067b010 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 143d845d074c4..7860bf15dc8e5 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 6df7f4b3f6de8..feec6431ee3cf 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 59ae5efcba191..d8dec2a486298 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/scheduled_task_id.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/scheduled_task_id.ts index 607166203e35f..d008421381b14 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 a8aad06c999d0..8de4b3dc32a0c 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 3bb170937bafc..5b9d9d1bfe918 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 72680ef786e9e..dca999ab68902 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/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 9dcdc8a26af0e..fb872f775c5bb 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 36917706d719c..b22674d66db4c 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 5ca8ac3bcd9f7..65e82a2e4fbf3 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 e68de4418c9bf..aefe3d0b1c873 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 85350fb43f1f2..1c801341a0e99 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 c901631388f11..a84c733586ec0 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 b92ddadc32c08..fbe2672c17cbe 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 65f3a36cbe487..3daa29c02b107 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 177d5ffc06486..f5feab6f04557 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 1f58d4b72cea8..060b7dda22dea 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 44245f9b10e12..c10c7a6b63997 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/fleet_api_integration/apis/agents/reassign.ts b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts index 7c97ff5b79bf9..746a2fcda1ba1 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 93d0a58b848df..3956dcafad705 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 2de75be2e50b0..afe9f8d677d35 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 9b0a6586e1c25..b842f89c8ac64 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 a48bc7651a1ed..831f0475c2c11 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/services/cases/api.ts b/x-pack/test/functional/services/cases/api.ts index ad4678adcafc3..983ee667a2ef8 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 5b854979adfc1..8a61358d04521 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 e46201b1996c1..872113f1a51be 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 b4cfee637cb46..8ecabdac8c4c5 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 95b0a746db8ca..a5f650198cf22 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 a54be7896877e..8d3ba0e73a24c 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 2db687f514778..6bdd35ee642e5 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/observability/alerts/common.ts b/x-pack/test/functional/services/observability/alerts/common.ts index 6491c7a8b0595..7a3f1f609a403 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( @@ -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 8990641cb524b..2b2c82c4f3728 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 0000000000000..e26e23e1e82db --- /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 0000000000000..5c34d4afce99e --- /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 282072dbb8dce..8d213e5b78075 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 a825bda9b90ee..ec8e05ceb9b9d 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 223127125e66d..52540169a4c8d 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 56e1f9fd62a08..c7918341da86e 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 60a4c2a571a1c..e797d6812ed8d 100644 --- a/x-pack/test/observability_functional/apps/observability/index.ts +++ b/x-pack/test/observability_functional/apps/observability/index.ts @@ -8,16 +8,16 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { - // FAILING: https://github.com/elastic/kibana/issues/140437 - describe.skip('ObservabilityApp', function () { + 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/alerts/index.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/index.ts index f2a59d6b22b2e..cdb0ea37a6417 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/alerts/index.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/index.ts @@ -20,8 +20,7 @@ export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const find = getService('find'); - // Failing: See https://github.com/elastic/kibana/issues/140248 - describe.skip('Observability alerts', function () { + describe('Observability alerts', function () { this.tags('includeFirefox'); const testSubjects = getService('testSubjects'); 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 443e0616cabe2..15c960c16f749 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 340131c14b6a1..96e989a9173e7 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 0000000000000..d1b6a2ce62519 --- /dev/null +++ b/x-pack/test/observability_functional/apps/observability/pages/overview/alert_table.ts @@ -0,0 +1,60 @@ +/* + * Copyright 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'); + + describe('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 6d17b9c6e0920..6ab449873fc76 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 a8b96c617db58..9ba2c69885b2a 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/plugin_api_integration/test_suites/task_manager/migrations.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/migrations.ts index 04f34ab50a133..232db750d5425 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 cf720be74143a..56477f0e6edf0 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 0000000000000..6acbc14a47352 --- /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 e96239f37cdfb..e1046b2dca6d7 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_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index 8c8629002c93f..a5b13c083b278 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,13 +34,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'Actions', ], [ - 'Host-nyierkw2gu', + 'Host-dpu1a2r2yi', 'x', 'x', - 'Failure', - 'Windows', - '10.180.151.227, 10.44.18.210', - '7.1.9', + 'Warning', + 'Linux', + '10.2.17.24, 10.56.215.200,10.254.196.130', + '8.5.0', 'x', '', ], @@ -48,10 +48,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'Host-rs9wp4o6l9', 'x', 'x', - 'Warning', - 'Windows', - '10.218.38.118, 10.80.35.162', - '8.0.8', + 'Success', + 'Linux', + '10.138.79.131, 10.170.160.154', + '8.5.0', 'x', '', ], @@ -62,7 +62,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'Warning', 'Linux', '10.87.11.145, 10.117.106.109,10.242.136.97', - '7.13.1', + '8.5.0', 'x', '', ], 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 0000000000000..d9ff1179f62a9 --- /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 84f90830a3f14..6aab2457c5278 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 50e0b0821ec81..e29ae10a60df8 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 5c6e96b4ce0be..38e7d2a667cd2 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; + } }